from __future__ import annotations import abc import sys from argparse import ArgumentParser from typing import TYPE_CHECKING from .log import ERR from .Types import LoadTypes, Types if TYPE_CHECKING: from typing import Any from .App import App class AbstractCmd(abc.ABC): def __init__( self, parent: App | AbstractCmd, ) -> None: self.__parent: App | AbstractCmd | None = parent self.__app: App | None = None self.__children: list[Cmd] = [] self.__child_classes: list[type[Cmd]] = [] self.__parser: ArgumentParser | None = None def set_parent(self, parent: Any | Cmd): self.__parent = parent @property def name(self) -> str: return self._name() @abc.abstractmethod def _name(self) -> str: raise NotImplementedError('Called pure virtual base class method') @property def parent(self) -> App | AbstractCmd: if self.__parent is None: raise Exception(f'Tried to access inexistent parent of command {self.name}') return self.__parent @property def app(self) -> App: from .App import App if self.__app is None: parent = self.__parent while True: if parent is None: raise Exception( "Can't get application object from command without parent" ) if isinstance(parent, App): self.__app = parent break assert parent != parent.__parent, 'Assertion failed: Parent mismatch' parent = parent.__parent return self.__app @property def children(self) -> tuple[Cmd, ...]: return tuple(self.__children) @property def child_classes(self) -> tuple[type[Cmd], ...]: return tuple(self.__child_classes) @property def parser(self) -> ArgumentParser: if self.__parser is None: raise Exception(f'Tried to get a non-existing parser from {self}') return self.__parser # Don't use a setter decorator to force using a grepable method def set_parser(self, parser: ArgumentParser): self.__parser = parser def print_help(self, exit_status: int | None = None) -> None: self.parser.print_help() if exit_status is not None: sys.exit(exit_status) def add_subcommands(self, cmds: Cmd | list[Cmd] | Types | list[Types]) -> None: if isinstance(cmds, Cmd): assert False return if isinstance(cmds, list): for cmd in cmds: self.add_subcommands(cmd) return if isinstance(cmds, Types): try: for cmd_class in cmds: if cmd_class in self.__child_classes: continue self.__child_classes.append(cmd_class) cmd = cmd_class(self) self.__children.append(cmd) assert len(self.__children) == len(self.__child_classes) except Exception as e: cmds.dump(ERR, f'Failed to add subcommands ({str(e)})') raise return raise Exception(f'Tried to add sub-commands of unknown type {type(cmds)}') def load_subcommands( self, modules: str | list[str] | None = None, name_filter: str = r'Cmd[^.]' ) -> None: if modules is None: # Derive module search path for the calling module's subcommands # from the module path of the calling module itself modules = [type(self).__module__.replace('Cmd', '').lower()] elif isinstance(modules, str): modules = [modules] self.add_subcommands(LoadTypes(modules, type_name_filter = name_filter)) # -- Interface to derived classes # To be overridden by derived class in case the command does take arguments. # Will be called from App base class constructor and set up the parser hierarchy def add_arguments(self, parser: ArgumentParser) -> None: pass @abc.abstractmethod async def _run(self, args) -> None: if isinstance(self.__parent, Cmd): # Calling App.run() would loop return await self.__parent._run(args) async def run(self, args): return await self._run(args) @abc.abstractmethod def _help(self) -> str: raise NotImplementedError('Called pure virtual base class method') @property def help(self) -> str: return self._help() def _description(self) -> str: raise NotImplementedError('Called pure virtual base class method') @property def description(self) -> str: return self._description() class Cmd(AbstractCmd): # export def __init__( self, parent: App | Cmd, name: str, help: str, description: str | None = None ) -> None: super().__init__(parent) self.__name = name self.__help = help self.__description = description if description else help def _name(self) -> str: return self.__name def _help(self) -> str: return self.__help def _description(self) -> str: return self.__description