lib.App: Add a root command slot
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m23s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m28s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 3m58s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m16s
CI / Packaging test (push) Successful in 0s
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m23s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m28s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 3m58s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m16s
CI / Packaging test (push) Successful in 0s
The application's top-level behavior is defined by overriding App._add_arguments() and App._run(). The lightweight run-and-options unit, Cmd, can already be mounted at any node of the command tree, but the root is reserved for the application itself. An application that wants to host a plain command at the top level therefore has to subclass App and carry its full lifecycle implementation. Add a root parameter to App.__init__(). When it is given a command class, App instantiates it and uses it as the top level: the command's options are registered on the top-level parser, it becomes the parent of the top-level subcommands, and App._run() delegates the run to it. The command's children are wired as the top-level subcommands, so the same Cmd can now occupy the root node. When root is not given, the previous auto-discovery behavior is preserved unchanged. Keep the top-level subcommand heading as plain "Available subcommands" whether it is hosted by the application or by a root command, while nested command levels continue to qualify the heading with the parent name. Add a unit test that mounts a root command hosting a child and checks option registration, dispatch, setup and teardown, and resolution of the application through the parent chain. 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
24e3059d92
commit
6b4bcdfaf8
3 changed files with 171 additions and 11 deletions
|
|
@ -112,6 +112,7 @@ class App: # export
|
|||
name_filter: str = '^Cmd.*',
|
||||
modules: list[str] | None = None,
|
||||
eloop: asyncio.AbstractEventLoop | None = None,
|
||||
root: type[AbstractCmd] | None = None,
|
||||
) -> None:
|
||||
|
||||
self.__args: Namespace | None = None
|
||||
|
|
@ -143,12 +144,22 @@ class App: # export
|
|||
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]
|
||||
if root is not None:
|
||||
# -- The application's top-level behavior is delegated to a root
|
||||
# command. The root hosts the top-level options and its
|
||||
# subcommands become the top-level subcommands. It is the same
|
||||
# kind of command that can be mounted at any other node in the
|
||||
# tree.
|
||||
self.__root: AbstractCmd | None = root(self)
|
||||
self.__cmds: Collection[AbstractCmd] = list(self.__root.children)
|
||||
else:
|
||||
self.__root = None
|
||||
cmd_classes: LoadTypes[AbstractCmd] = LoadTypes(
|
||||
modules if modules else ['__main__'],
|
||||
type_name_filter = name_filter,
|
||||
type_filter = [AbstractCmd],
|
||||
)
|
||||
self.__cmds = [cmd_class(self) for cmd_class in cmd_classes]
|
||||
self._build_parser()
|
||||
|
||||
def _build_parser(self, argv: list[str] | None = None) -> None:
|
||||
|
|
@ -172,13 +183,17 @@ class App: # export
|
|||
parent: AbstractCmd | App,
|
||||
parser: ArgumentParser,
|
||||
cmds: Collection[AbstractCmd],
|
||||
all: bool = False
|
||||
all: bool,
|
||||
top_level: bool,
|
||||
) -> None:
|
||||
if not cmds:
|
||||
return
|
||||
|
||||
title = 'Available subcommands'
|
||||
if isinstance(parent, AbstractCmd):
|
||||
# -- Only nested command levels qualify the title with the parent
|
||||
# name; the top level (hosted by the application itself or by a
|
||||
# root command) keeps the plain title.
|
||||
if not top_level and isinstance(parent, AbstractCmd):
|
||||
title += ' of ' + parent.name
|
||||
subparsers = parser.add_subparsers(
|
||||
title = title, metavar = '', dest = 'command'
|
||||
|
|
@ -203,7 +218,11 @@ class App: # export
|
|||
if id(sc) not in seen:
|
||||
seen.add(id(sc))
|
||||
add_cmds_to_parser(
|
||||
sc.cmd, sc.parser, sc.cmd.children, all = all
|
||||
sc.cmd,
|
||||
sc.parser,
|
||||
sc.cmd.children,
|
||||
all = all,
|
||||
top_level = False,
|
||||
)
|
||||
return
|
||||
# -- Re-parse the command line to find the invoked subcommand.
|
||||
|
|
@ -214,7 +233,13 @@ class App: # export
|
|||
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)
|
||||
add_cmds_to_parser(
|
||||
sc.cmd,
|
||||
sc.parser,
|
||||
sc.cmd.children,
|
||||
all = all,
|
||||
top_level = False,
|
||||
)
|
||||
|
||||
cmdline = sys.argv if argv is None else argv
|
||||
if argv is None:
|
||||
|
|
@ -229,6 +254,12 @@ class App: # export
|
|||
add_help = False,
|
||||
)
|
||||
self._add_arguments(self.__parser)
|
||||
if self.__root is not None:
|
||||
# -- The root command hosts the top-level behavior: register its
|
||||
# options on the top-level parser and make it the parent of the
|
||||
# top-level subcommands.
|
||||
self.__root.set_parser(self.__parser)
|
||||
self.__root.add_arguments(self.__parser)
|
||||
|
||||
if not add_all_parsers:
|
||||
# Parse known args and configure logging, but only if we're not on
|
||||
|
|
@ -241,7 +272,22 @@ class App: # export
|
|||
set_log_level(args.log_level)
|
||||
log(DEBUG, f'-------------- Running: >{pretty_cmd(cmdline)}<')
|
||||
|
||||
add_cmds_to_parser(self, self.__parser, self.__cmds, all = add_all_parsers)
|
||||
if self.__root is None:
|
||||
add_cmds_to_parser(
|
||||
self,
|
||||
self.__parser,
|
||||
self.__cmds,
|
||||
all = add_all_parsers,
|
||||
top_level = False,
|
||||
)
|
||||
else:
|
||||
add_cmds_to_parser(
|
||||
self.__root,
|
||||
self.__parser,
|
||||
self.__root.children,
|
||||
all = add_all_parsers,
|
||||
top_level = True,
|
||||
)
|
||||
|
||||
# -- Add help only now, wouldn't want to have parse_known_args() exit
|
||||
# on --help with subcommands missing
|
||||
|
|
@ -343,6 +389,10 @@ class App: # export
|
|||
# want to do something else, for instance if you don't have sub-commands,
|
||||
# or if want to do anything before and / or after the subcommands.
|
||||
async def _run(self, args: Namespace) -> None | int:
|
||||
if self.__root is not None:
|
||||
# -- Delegate the top-level behavior (options, setup, dispatch,
|
||||
# teardown) to the root command.
|
||||
return cast('None | int', await self.__root.run(args))
|
||||
if not hasattr(args, 'func'):
|
||||
self.__parser.print_help()
|
||||
return None
|
||||
|
|
|
|||
Loading…
Reference in a new issue