2026-01-20 19:01:15 +01:00
|
|
|
# -*- 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 *
|
2026-05-01 10:17:24 +02:00
|
|
|
from .Types import Types, LoadTypes
|
2026-01-20 19:01:15 +01:00
|
|
|
|
|
|
|
|
class Cmd(abc.ABC): # export
|
|
|
|
|
|
2026-04-24 11:53:49 +02:00
|
|
|
def __init__(self, parent: App|Cmd, name: str, help: str, description: str|None=None) -> None:
|
2026-01-20 19:01:15 +01:00
|
|
|
from . import App
|
2026-01-27 10:22:16 +01:00
|
|
|
self.__parent: App|Cmd|None = parent
|
2026-01-20 19:01:15 +01:00
|
|
|
self.__app: App|None = None
|
|
|
|
|
self.__name = name
|
|
|
|
|
self.__help = help
|
2026-04-24 11:53:49 +02:00
|
|
|
self.__description = description if description else help
|
2026-01-20 19:01:15 +01:00
|
|
|
self.__children: list[Cmd] = []
|
|
|
|
|
self.__child_classes: list[type[Cmd]] = []
|
2026-02-23 07:19:45 +01:00
|
|
|
self.__parser: ArgumentParser|None = None
|
2026-01-20 19:01:15 +01:00
|
|
|
|
2026-02-20 12:04:10 +01:00
|
|
|
@abc.abstractmethod
|
|
|
|
|
async def _run(self, args) -> None:
|
|
|
|
|
if isinstance(self.__parent, Cmd): # Calling App.run() would loop
|
|
|
|
|
return await self.__parent._run(args)
|
2026-01-20 19:01:15 +01:00
|
|
|
|
|
|
|
|
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
|
2026-01-27 10:22:16 +01:00
|
|
|
def app(self) -> App:
|
2026-01-20 19:01:15 +01:00
|
|
|
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
|
2026-04-25 07:45:14 +02:00
|
|
|
assert parent != parent.__parent, f'Assertion failed: Parent mismatch'
|
2026-01-20 19:01:15 +01:00
|
|
|
parent = parent.__parent
|
|
|
|
|
return self.__app
|
|
|
|
|
|
2026-02-23 07:19:45 +01:00
|
|
|
# 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
|
|
|
|
|
|
2026-01-20 19:01:15 +01:00
|
|
|
@property
|
|
|
|
|
def name(self) -> str:
|
|
|
|
|
return self.__name
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def help(self) -> str:
|
|
|
|
|
return self.__help
|
|
|
|
|
|
2026-04-24 11:53:49 +02:00
|
|
|
@property
|
|
|
|
|
def description(self) -> str:
|
|
|
|
|
return self.__description
|
|
|
|
|
|
2026-01-20 19:01:15 +01:00
|
|
|
@property
|
|
|
|
|
def children(self) -> list[Cmd]:
|
|
|
|
|
return tuple(self.__children)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def child_classes(self) -> list[type[Cmd]]:
|
|
|
|
|
return tuple(self.__child_classes)
|
|
|
|
|
|
2026-05-01 10:17:38 +02:00
|
|
|
def print_help(self, exit_status: int|None=None) -> None:
|
|
|
|
|
self.parser.print_help()
|
|
|
|
|
if exit_status is not None:
|
|
|
|
|
sys.exit(exit_status)
|
|
|
|
|
|
2026-01-20 19:01:15 +01:00
|
|
|
async def run(self, args):
|
2026-02-09 15:04:45 +01:00
|
|
|
return await self._run(args)
|
|
|
|
|
|
2026-01-20 19:01:15 +01:00
|
|
|
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)
|
2026-01-27 10:22:16 +01:00
|
|
|
cmd = cmd_class(self)
|
2026-01-20 19:01:15 +01:00
|
|
|
self.__children.append(cmd)
|
|
|
|
|
assert len(self.__children) == len(self.__child_classes)
|
|
|
|
|
except Exception as e:
|
2026-02-18 11:23:37 +01:00
|
|
|
cmds.dump(ERR, f"Failed to add subcommands ({str(e)})")
|
2026-01-20 19:01:15 +01:00
|
|
|
raise
|
|
|
|
|
return
|
|
|
|
|
raise Exception(f'Tried to add sub-commands of unknown type {type(cmds)}')
|
|
|
|
|
|
2026-05-01 10:17:24 +02:00
|
|
|
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))
|
|
|
|
|
|
2026-01-20 19:01:15 +01:00
|
|
|
# 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
|