jw-pkg/src/python/jw/pkg/lib/Cmd.py

126 lines
4.2 KiB
Python
Raw Normal View History

# -*- 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