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 <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-08-14 23:38:04 +02:00
commit 3fce4b27f8
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -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()