jw-pkg/src/python/jw/pkg/lib/Cmd.py
Jan Lindemann 5d1ba6e15a
pyproject.toml: Enforce import annotations style

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>
2026-06-01 14:34:25 +02:00

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