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
|
import warnings
|
||||||
|
|
||||||
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
|
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 .AsyncRunner import AsyncRunner
|
||||||
|
from .Cmd import AbstractCmd
|
||||||
from .log import (
|
from .log import (
|
||||||
DEBUG,
|
DEBUG,
|
||||||
ERR,
|
ERR,
|
||||||
NOTICE,
|
NOTICE,
|
||||||
|
WARNING,
|
||||||
LogFlag,
|
LogFlag,
|
||||||
log,
|
log,
|
||||||
log_m,
|
log_m,
|
||||||
|
|
@ -43,6 +45,11 @@ def _get_current_event_loop() -> asyncio.AbstractEventLoop | None:
|
||||||
except (RuntimeError, DeprecationWarning):
|
except (RuntimeError, DeprecationWarning):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
class _SubCommand(NamedTuple):
|
||||||
|
|
||||||
|
cmd: AbstractCmd
|
||||||
|
parser: ArgumentParser
|
||||||
|
|
||||||
class App: # export
|
class App: # export
|
||||||
|
|
||||||
def _add_arguments(self, parser: ArgumentParser) -> None:
|
def _add_arguments(self, parser: ArgumentParser) -> None:
|
||||||
|
|
@ -105,8 +112,6 @@ class App: # export
|
||||||
eloop: asyncio.AbstractEventLoop | None = None,
|
eloop: asyncio.AbstractEventLoop | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
from .Cmd import AbstractCmd
|
|
||||||
|
|
||||||
self.__args: Namespace | None = None
|
self.__args: Namespace | None = None
|
||||||
self.__cmdline: str | None = None
|
self.__cmdline: str | None = None
|
||||||
self.__description = description
|
self.__description = description
|
||||||
|
|
@ -171,23 +176,25 @@ class App: # export
|
||||||
if not cmds:
|
if not cmds:
|
||||||
return
|
return
|
||||||
|
|
||||||
class SubCommand:
|
|
||||||
|
|
||||||
def __init__(self, cmd: AbstractCmd, parser: Any):
|
|
||||||
self.cmd = cmd
|
|
||||||
self.parser = parser
|
|
||||||
|
|
||||||
title = 'Available subcommands'
|
title = 'Available subcommands'
|
||||||
if isinstance(parent, AbstractCmd):
|
if isinstance(parent, AbstractCmd):
|
||||||
title += ' of ' + parent.name
|
title += ' of ' + parent.name
|
||||||
subparsers = parser.add_subparsers(
|
subparsers = parser.add_subparsers(
|
||||||
title = title, metavar = '', dest = 'command'
|
title = title, metavar = '', dest = 'command'
|
||||||
)
|
)
|
||||||
scs: dict[str, SubCommand] = {}
|
scs: dict[str, _SubCommand] = {}
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
cmd.set_parent(parent)
|
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:
|
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]
|
scs[alias] = scs[cmd.name]
|
||||||
if all:
|
if all:
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
|
|
@ -198,14 +205,16 @@ class App: # export
|
||||||
sc.cmd, sc.parser, sc.cmd.children, all = all
|
sc.cmd, sc.parser, sc.cmd.children, all = all
|
||||||
)
|
)
|
||||||
return
|
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)
|
args, _ = self.__parser.parse_known_args(argv)
|
||||||
cmd_name = getattr(args, 'command', None)
|
cmd_name = getattr(args, 'command', None)
|
||||||
if cmd_name in scs:
|
if cmd_name in scs:
|
||||||
sc = scs[cmd_name]
|
sc = scs[cmd_name]
|
||||||
add_cmds_to_parser(sc.cmd, sc.parser, sc.cmd.children, all = all)
|
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
|
cmdline = sys.argv if argv is None else argv
|
||||||
if argv is None:
|
if argv is None:
|
||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue