# -*- coding: utf-8 -*- from __future__ import annotations from typing import Any import inspect, sys, re, abc, argparse from argparse import ArgumentParser, _SubParsersAction from .log import * from .Types import Types, LoadTypes class Cmd(abc.ABC): # export def __init__(self, parent: App|Cmd, name: str, help: str, description: str|None=None) -> None: from . import App self.__parent: App|Cmd|None = parent self.__app: App|None = None self.__name = name self.__help = help self.__description = description if description else help self.__children: list[Cmd] = [] self.__child_classes: list[type[Cmd]] = [] self.__parser: ArgumentParser|None = None @abc.abstractmethod async def _run(self, args) -> None: if isinstance(self.__parent, Cmd): # Calling App.run() would loop return await self.__parent._run(args) def set_parent(self, parent: Any|Cmd): self.__parent = parent @property def parent(self) -> App|Cmd: 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, f'Assertion failed: Parent mismatch' parent = parent.__parent return self.__app # Don't use a setter decorator to force using a grepable method def set_parser(self, parser: ArgumentParser): self.__parser = parser @property def parser(self) -> str: return self.__parser @property def name(self) -> str: return self.__name @property def help(self) -> str: return self.__help @property def description(self) -> str: return self.__description @property def children(self) -> list[Cmd]: return tuple(self.__children) @property def child_classes(self) -> list[type[Cmd]]: return tuple(self.__child_classes) def print_help(self, exit_status: int|None=None) -> None: self.parser.print_help() if exit_status is not None: sys.exit(exit_status) async def run(self, args): return await self._run(args) def add_subcommands(self, cmds: Cmd|list[Cmds]|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)) # 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