lib.App: Find invoked path by walking argv
All checks were successful
CI / Packaging - Kali Linux (push) Successful in 3m36s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 3m47s
CI / Packaging test (push) Successful in 0s

The discovery re-parse re-parses the full command line at each level
against the subparsers registered so far. A level's subcommand parsers
are not registered until the re-parse descends into them, so the tokens
after the subcommand name are parsed against the current level's
options. An option meant for a deeper level can then collide with a
same-named option of a shallower level, or fail on a missing value.

Replace the re-parse with a single argv walk. The subcommand names at
each level are known once the commands are materialized, so the invoked
path is found by matching tokens against those names and skipping the
options (and the values they consume) that precede them. Walking the
tokens never parses the tail against a half-built parser, so the
collision cannot occur.

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-18 00:31:36 +02:00
commit e3cff8104f

View file

@ -36,6 +36,11 @@ if TYPE_CHECKING:
from typing import TypeVar
T = TypeVar('T')
class _SubCommand(NamedTuple):
cmd: AbstractCmd
parser: ArgumentParser
def _get_current_event_loop() -> asyncio.AbstractEventLoop | None:
"""Return the current event loop of this thread, or None if there is
none, without creating one implicitly or emitting a deprecation
@ -47,10 +52,63 @@ def _get_current_event_loop() -> asyncio.AbstractEventLoop | None:
except (RuntimeError, DeprecationWarning):
return None
class _SubCommand(NamedTuple):
def _build_option_map(parser: ArgumentParser) -> dict[str, int | str | None]:
"""Map each option string to its nargs, so the invoked-path walk can tell
which options consume the following token (a value) and which are bare
flags."""
opt_map: dict[str, int | str | None] = {}
for action in parser._actions: # noqa: SLF001
for opt in getattr(action, 'option_strings', ()):
opt_map[opt] = action.nargs
return opt_map
cmd: AbstractCmd
parser: ArgumentParser
def _find_invoked_subcommand(
opt_map: dict[str, int | str | None],
names: set[str],
argv: list[str],
pos: int,
) -> tuple[str | None, int]:
"""Find the invoked subcommand name in argv starting at pos.
Walk the argv, skipping options and the tokens they consume (using
opt_map), and return the first positional token that is a subcommand
name, along with the position just past it. Return (None, pos) if no
subcommand name is present at or after pos.
"""
n = len(argv)
i = pos
while i < n:
tok = argv[i]
if tok.startswith('-') and len(tok) > 1:
if '=' in tok:
# --opt=value: the value is inline, consume the token only.
i += 1
elif tok in opt_map and opt_map[tok] != 0:
nargs = opt_map[tok]
if nargs in ('+', '*') or (isinstance(nargs, int) and nargs > 1):
# -- Multi-value option: consume the following non-option,
# non-subcommand tokens (its values).
j = i + 1
while (j < n and not argv[j].startswith('-')
and argv[j] not in names):
j += 1
i = j
elif i + 1 < n and not argv[i + 1].startswith('-'):
# -- One value: consume the following token.
i += 2
else:
i += 1
else:
# -- Flag or unknown option: consume the token only.
i += 1
continue
if tok in names:
return tok, i + 1
# -- A positional token that is not a subcommand name: the invoked
# path ends here.
return None, i
# -- Ran off the end of the argv consuming option values: no subcommand.
return None, i
class App: # export
@ -185,6 +243,7 @@ class App: # export
cmds: Collection[AbstractCmd],
all: bool,
top_level: bool,
pos: int,
) -> None:
if not cmds:
return
@ -223,15 +282,24 @@ class App: # export
sc.cmd.children,
all = all,
top_level = False,
pos = pos,
)
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:
# -- Find the invoked subcommand by walking the argv. The
# subcommand names at this level are known (the commands are
# materialized), so the invoked path is found by matching tokens
# against those names and skipping the options (and their
# values) that precede them. Walking the tokens never parses the
# tail against a half-built parser, so a deeper option cannot
# collide with a same-named option of a shallower level the way
# a discovery re-parse would.
cmd_name, new_pos = _find_invoked_subcommand(
_build_option_map(parser),
set(scs.keys()),
argv_list,
pos,
)
if cmd_name is not None and cmd_name in scs:
sc = scs[cmd_name]
add_cmds_to_parser(
sc.cmd,
@ -239,11 +307,16 @@ class App: # export
sc.cmd.children,
all = all,
top_level = False,
pos = new_pos,
)
cmdline = sys.argv if argv is None else argv
if argv is None:
argv = sys.argv[1:]
# -- The argv the invoked-path walk (below) operates on. A separate
# binding so the nested closure sees a plain list[str], not the
# optional parameter type.
argv_list: list[str] = argv
add_all_parsers = (
'-h' in argv or '--help' in argv or '_ARGCOMPLETE' in os.environ
)
@ -279,6 +352,7 @@ class App: # export
self.__cmds,
all = add_all_parsers,
top_level = False,
pos = 0,
)
else:
add_cmds_to_parser(
@ -287,6 +361,7 @@ class App: # export
self.__root.children,
all = add_all_parsers,
top_level = True,
pos = 0,
)
# -- Add help only now, wouldn't want to have parse_known_args() exit