From 3fce4b27f8c05963145aef7ff7dd7489cc93660d Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Fri, 14 Aug 2026 23:38:04 +0200 Subject: [PATCH] lib.App: Rebuild parser from run() argv __init__() builds the parser and the lazy subcommand registration inside it decides which subcommands to register by re-parsing sys.argv. run() then parses a different argv, so if the caller passes an argv that is deeper than the one in sys.argv, the required subparsers have not been registered and the invocation fails with an "unrecognized arguments" error. run_sub_commands() passes argv to run(), so the mismatch is reachable from the public API. Move the parser construction from __init__() into _build_parser() and call it from run() when an argv is given, so that registration and parsing are driven by the same command line. The top-level command instances are created once in __init__() and reused when the parser is rebuilt. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/App.py | 97 ++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index 263938c2..b106b216 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -105,6 +105,47 @@ 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 + + self.__default_log_flags = self._default_log_flags( + LogFlag.STDERR | LogFlag.POSITION | LogFlag.PRIO | LogFlag.COLOR + ) + if (env := os.getenv(self._default_log_flags_env(), None)) is not None: + self.__default_log_flags = parse_log_flags(env) + + self.__default_log_level = self._default_log_level(NOTICE) + if (env := os.getenv(self._default_log_level_env(), None)) is not None: + self.__default_log_level = parse_log_level(env) + + self.__default_log_file = self._default_log_file(None) + if (env := os.getenv(self._default_log_file_env(), None)) is not None: + self.__default_log_file = env + + self.__back_trace = self._default_show_backtrace(False) + if (env := os.getenv(self._default_show_backtrace_env())) is not None: + self.__back_trace = env.lower() in ['1', 'true'] + + set_log_flags(self.__default_log_flags) + set_log_level(self.__default_log_level) + + self.__async_runner: AsyncRunner | None = None + self.__eloop = eloop + self.__own_eloop = False + + cmd_classes: LoadTypes[AbstractCmd] = LoadTypes( + modules if modules else ['__main__'], + type_name_filter = name_filter, + type_filter = [AbstractCmd], + ) + self.__cmds: list[AbstractCmd] = [cmd_class(self) for cmd_class in cmd_classes] + self._build_parser() + + def _build_parser(self, argv: list[str] | None = None) -> None: + def add_cmd_to_parser(cmd: AbstractCmd, parsers: Any) -> ArgumentParser: parser = cast( 'ArgumentParser', @@ -157,7 +198,7 @@ class App: # export sc.cmd, sc.parser, sc.cmd.children, all = all ) return - args, _ = self.__parser.parse_known_args() + args, _ = self.__parser.parse_known_args(argv) cmd_name = getattr(args, 'command', None) if cmd_name in scs: sc = scs[cmd_name] @@ -165,61 +206,27 @@ class App: # export from .Cmd import AbstractCmd - self.__args: Namespace | None = None - self.__cmdline: str | None = None - - self.__default_log_flags = self._default_log_flags( - LogFlag.STDERR | LogFlag.POSITION | LogFlag.PRIO | LogFlag.COLOR + cmdline = sys.argv if argv is None else argv + if argv is None: + argv = sys.argv[1:] + add_all_parsers = ( + '-h' in argv or '--help' in argv or '_ARGCOMPLETE' in os.environ ) - if (env := os.getenv(self._default_log_flags_env(), None)) is not None: - self.__default_log_flags = parse_log_flags(env) - - self.__default_log_level = self._default_log_level(NOTICE) - if (env := os.getenv(self._default_log_level_env(), None)) is not None: - self.__default_log_level = parse_log_level(env) - - self.__default_log_file = self._default_log_file(None) - if (env := os.getenv(self._default_log_file_env(), None)) is not None: - self.__default_log_file = env - - self.__back_trace = self._default_show_backtrace(False) - if (env := os.getenv(self._default_show_backtrace_env())) is not None: - self.__back_trace = env.lower() in ['1', 'true'] - - set_log_flags(self.__default_log_flags) - set_log_level(self.__default_log_level) - - self.__async_runner: AsyncRunner | None = None - self.__eloop = eloop - self.__own_eloop = False self.__parser = ArgumentParser( formatter_class = ArgumentDefaultsHelpFormatter, - description = description, + description = self.__description, add_help = False, ) self._add_arguments(self.__parser) - args, _ = self.__parser.parse_known_args() + args, _ = self.__parser.parse_known_args(argv) set_log_flags(args.log_flags) set_log_level(args.log_level) - log(DEBUG, f'-------------- Running: >{pretty_cmd(sys.argv)}<') + log(DEBUG, f'-------------- Running: >{pretty_cmd(cmdline)}<') - cmd_classes: LoadTypes[AbstractCmd] = LoadTypes( - modules if modules else ['__main__'], - type_name_filter = name_filter, - type_filter = [AbstractCmd], - ) - add_all_parsers = ( - '-h' in sys.argv or '--help' in sys.argv or '_ARGCOMPLETE' in os.environ - ) - add_cmds_to_parser( - self, - self.__parser, - [cmd_class(self) for cmd_class in cmd_classes], - all = add_all_parsers, - ) + add_cmds_to_parser(self, self.__parser, self.__cmds, all = add_all_parsers) # -- Add help only now, wouldn't want to have parse_known_args() exit # on --help with subcommands missing @@ -361,6 +368,8 @@ class App: # export self.__eloop = eloop self.__own_eloop = True try: + if argv is not None: + self._build_parser(argv) ret = self.eloop.run_until_complete(self.__run(argv)) finally: self.close()