lib.Cmd: Build the command tree lazily
App() construction builds the entire command tree: every load_subcommands() call in a command's __init__() constructs its whole subtree eagerly, so running a single leaf command pays to instantiate every unrelated command object as well (jw-pkg builds about 50 command objects for any invocation). Defer the construction instead. load_subcommands() now records only the module search path and name filter, and the subcommands are materialized on first access to the children or child_classes property. The parser reads children only down the invoked branch on the non-help path, so a simple run instantiates just that path (jw-pkg builds 5-7 objects), while the help and completion path expands every node and leaves rendered help unchanged. 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
3ae8738b36
commit
9f7e1b6cb2
1 changed files with 24 additions and 1 deletions
|
|
@ -25,6 +25,12 @@ class AbstractCmd(abc.ABC):
|
|||
self.__children: list[Cmd] = []
|
||||
self.__child_classes: list[type[Cmd]] = []
|
||||
self.__parser: ArgumentParser | None = None
|
||||
# -- Subcommands registered via load_subcommands() are not built
|
||||
# immediately; the module search path and name filter are stored here
|
||||
# and the subcommands are materialized on first access to `children`
|
||||
# (see __materialize_pending_subcommands()). This keeps a simple run
|
||||
# from instantiating the whole command tree.
|
||||
self.__pending_subcommands: tuple[list[str], str] | None = None
|
||||
|
||||
def set_parent(self, parent: Any | Cmd) -> None:
|
||||
self.__parent = parent
|
||||
|
|
@ -61,12 +67,25 @@ class AbstractCmd(abc.ABC):
|
|||
parent = parent.__parent
|
||||
return self.__app
|
||||
|
||||
def __materialize_pending_subcommands(self) -> None:
|
||||
# -- Build the subcommands that load_subcommands() registered
|
||||
# lazily, if any. Called on first access to `children` (and
|
||||
# `child_classes`) so that only the parts of the tree a run actually
|
||||
# descends into are ever instantiated.
|
||||
if self.__pending_subcommands is None:
|
||||
return
|
||||
modules, name_filter = self.__pending_subcommands
|
||||
self.__pending_subcommands = None
|
||||
self.add_subcommands(LoadTypes(modules, type_name_filter = name_filter))
|
||||
|
||||
@property
|
||||
def children(self) -> tuple[Cmd, ...]:
|
||||
self.__materialize_pending_subcommands()
|
||||
return tuple(self.__children)
|
||||
|
||||
@property
|
||||
def child_classes(self) -> tuple[type[Cmd], ...]:
|
||||
self.__materialize_pending_subcommands()
|
||||
return tuple(self.__child_classes)
|
||||
|
||||
@property
|
||||
|
|
@ -127,7 +146,11 @@ class AbstractCmd(abc.ABC):
|
|||
modules = [type(self).__module__.replace('Cmd', '').lower()]
|
||||
elif isinstance(modules, str):
|
||||
modules = [modules]
|
||||
self.add_subcommands(LoadTypes(modules, type_name_filter = name_filter))
|
||||
# -- Defer the actual subcommand construction: store the search path
|
||||
# and filter, and materialize on first access to `children`. Building
|
||||
# them here (in __init__) would instantiate the entire tree up front,
|
||||
# even for a run that only descends into a single branch.
|
||||
self.__pending_subcommands = (modules, name_filter)
|
||||
|
||||
# -- Interface to derived classes
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue