Add new ruff rules and fix their fallout:
future-annotations = true
select = [ "TC", # type-checking import placement rules "FA", # future annotations rules ]This comprises:
- Streamline imports and exports in cmds.xxx.Cmd
- Import base class as "Base"- Export types Cmd and Parent via __all__- Move all types imported only for annotation below TYPE_CHECKING
- Use "from __future__ import annotations" all over the place
Signed-off-by: Jan Lindemann <jan@janware.com>
174 lines
5.2 KiB
Python
174 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import abc
|
|
import sys
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .log import ERR
|
|
from .Types import LoadTypes, Types
|
|
|
|
if TYPE_CHECKING:
|
|
from argparse import ArgumentParser
|
|
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
|