"""CLI dispatcher — builds parser trees from command dataclasses.""" from __future__ import annotations import logging from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser from typing import Any, Dict, List, Optional, Type from byteb4rb1e.utils.argparse.command import CLICommand class CLI: """Composable CLI built from a tree of Command dataclasses. Recursively bootstraps an argparse parser hierarchy and tracks dest names so ``run()`` can dispatch to the correct leaf command without dest chaining in the caller. Usage:: cli = CLI(prog="repository", description="...") cli.bootstrap([MirrorCommand, IndexCommand]) cli.run() """ def __init__( self, prog: Optional[str] = None, description: str = "", ) -> None: kwargs = {} # type: Dict[str, Any] if prog: kwargs["prog"] = prog if description: kwargs["description"] = description kwargs.setdefault( "formatter_class", ArgumentDefaultsHelpFormatter, ) self.parser = ArgumentParser(**kwargs) self._dests = [] # type: List[str] self._commands = {} # type: Dict[str, Command] def add_arguments(self, parser: ArgumentParser) -> None: """Add global arguments to the root parser.""" parser.add_argument( "-v", "--verbose", action="count", default=0, help="Increase verbosity (-v for INFO, -vv for DEBUG)", ) def bootstrap( self, commands: List[Type[Command]], ) -> None: """Build the parser tree from a list of top-level commands.""" self.add_arguments(self.parser) dest = "command" self._dests.append(dest) sub = self.parser.add_subparsers(dest=dest) for cmd_cls in commands: self._add(sub, cmd_cls, prefix="") def _add( self, subparsers: Any, cmd_cls: Type[Command], prefix: str, ) -> None: """Recursively add a command and its subcommands.""" cmd = cmd_cls() parser = subparsers.add_parser( cmd.name, formatter_class=ArgumentDefaultsHelpFormatter, **cmd.parser_kwargs(), ) cmd.add_arguments(parser) key = "%s.%s" % (prefix, cmd.name) if prefix else cmd.name self._commands[key] = cmd if cmd._subcommands: dest = "%s_command" % cmd.name self._dests.append(dest) child_sub = parser.add_subparsers(dest=dest) for sc_cls in cmd._subcommands: self._add(child_sub, sc_cls, prefix=key) def _resolve(self, args: Any) -> Optional[Command]: """Walk dest chain to find the leaf command.""" parts = [] # type: List[str] for dest in self._dests: val = getattr(args, dest, None) if val is None: continue parts.append(val) if not parts: return None key = ".".join(parts) return self._commands.get(key) @staticmethod def _setup_logging(verbosity: int) -> None: if verbosity >= 2: level = logging.DEBUG elif verbosity >= 1: level = logging.INFO else: level = logging.WARNING logging.basicConfig( level=level, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler()], ) def run(self) -> None: """Parse args and dispatch to the leaf command.""" args = self.parser.parse_args() self._setup_logging(getattr(args, "verbose", 0)) cmd = self._resolve(args) if cmd is None: self.parser.print_help() raise SystemExit(1) raise SystemExit(cmd.execute(args))