diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index 2df96c67..123aad2f 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -7,13 +7,15 @@ import sys import warnings from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace -from typing import TYPE_CHECKING, Any, cast, override +from typing import TYPE_CHECKING, Any, NamedTuple, cast, override from .AsyncRunner import AsyncRunner +from .Cmd import AbstractCmd from .log import ( DEBUG, ERR, NOTICE, + WARNING, LogFlag, log, log_m, @@ -43,6 +45,11 @@ def _get_current_event_loop() -> asyncio.AbstractEventLoop | None: except (RuntimeError, DeprecationWarning): return None +class _SubCommand(NamedTuple): + + cmd: AbstractCmd + parser: ArgumentParser + class App: # export def _add_arguments(self, parser: ArgumentParser) -> None: @@ -105,8 +112,6 @@ class App: # export eloop: asyncio.AbstractEventLoop | None = None, ) -> None: - from .Cmd import AbstractCmd - self.__args: Namespace | None = None self.__cmdline: str | None = None self.__description = description @@ -171,23 +176,25 @@ class App: # export if not cmds: return - class SubCommand: - - def __init__(self, cmd: AbstractCmd, parser: Any): - self.cmd = cmd - self.parser = parser - title = 'Available subcommands' if isinstance(parent, AbstractCmd): title += ' of ' + parent.name subparsers = parser.add_subparsers( title = title, metavar = '', dest = 'command' ) - scs: dict[str, SubCommand] = {} + scs: dict[str, _SubCommand] = {} for cmd in cmds: cmd.set_parent(parent) - scs[cmd.name] = SubCommand(cmd, add_cmd_to_parser(cmd, subparsers)) + if cmd.name in scs: + log(WARNING, f'Duplicate subcommand name: {cmd.name}') + scs[cmd.name] = _SubCommand(cmd, add_cmd_to_parser(cmd, subparsers)) for alias in cmd.aliases: + if alias != cmd.name and alias in scs: + log( + WARNING, + f'Subcommand alias "{alias}" of "{cmd.name}" ' + 'collides with an earlier subcommand', + ) scs[alias] = scs[cmd.name] if all: seen: set[int] = set() @@ -198,14 +205,16 @@ class App: # export sc.cmd, sc.parser, sc.cmd.children, all = all ) return + # -- Re-parse the command line to find the invoked subcommand. + # This works because every level below uses dest = 'command', + # so each pass descends one level further into the command + # tree. args, _ = self.__parser.parse_known_args(argv) cmd_name = getattr(args, 'command', None) if cmd_name in scs: sc = scs[cmd_name] add_cmds_to_parser(sc.cmd, sc.parser, sc.cmd.children, all = all) - from .Cmd import AbstractCmd - cmdline = sys.argv if argv is None else argv if argv is None: argv = sys.argv[1:]