lib.App: Tidy up subcommand registration
The subcommand registration in _build_parser() defines a SubCommand helper class inside the add_cmds_to_parser() closure, so a fresh class object is created on every call. It also stores command names and aliases in a dictionary without checking for duplicates, so a colliding name or alias is silently overwritten, and it relies on every subparser level sharing the dest = 'command' attribute to descend one level per re-parse, an invariant that is not documented anywhere. Hoist the helper to a module-level _SubCommand NamedTuple, log a warning when a subcommand name or alias collides with an earlier one at the same level, and document the dest = 'command' invariant next to the re-parse. 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:
parent
0be539a40a
commit
3ac649cdf1
1 changed files with 22 additions and 13 deletions
|
|
@ -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:]
|
||||
|
|
|
|||
Loading…
Reference in a new issue