diff --git a/src/python/jw/pkg/lib/Cmd.py b/src/python/jw/pkg/lib/Cmd.py index dc7147ad..7f9b87f3 100644 --- a/src/python/jw/pkg/lib/Cmd.py +++ b/src/python/jw/pkg/lib/Cmd.py @@ -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