lib.App, .Cmd: Add modules

Add App and Cmd as generic base classes for multi-command
applications. The code is taken from jw-python: The exising
jw.pkg.App is very similar to the more capable jwutils.Cmds class,
so, to avoid code duplication, add it here to allow for jwutils.Cmds
and jw.pkg.App to derive from it at some point in the future.

Both had to be slightly modified to work within jw-pkg's less
equipped context, and will need futher code cleanup.

Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-01-20 19:01:15 +01:00
commit 0be02c7154
2 changed files with 256 additions and 0 deletions

View file

@ -0,0 +1,107 @@
# -*- 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
# full blown example of one level of nested subcommands
# git -C project remote -v show -n myremote
class Cmd(abc.ABC): # export
def __init__(self, name: str, help: str) -> None:
from . import App
self.__app: App|None = None
self.__parent: App|Cmd|None = None
self.__name = name
self.__help = help
self.__children: list[Cmd] = []
self.__child_classes: list[type[Cmd]] = []
async def _run(self, args):
pass
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):
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
parent = parent.__parent
return self.__app
@property
def name(self) -> str:
return self.__name
@property
def help(self) -> str:
return self.__help
@property
def children(self) -> list[Cmd]:
return tuple(self.__children)
@property
def child_classes(self) -> list[type[Cmd]]:
return tuple(self.__child_classes)
@abc.abstractmethod
async def run(self, args):
pass
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()
cmd.set_parent(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 ({e})")
raise
return
raise Exception(f'Tried to add sub-commands of unknown type {type(cmds)}')
# 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
def conf_value(self, path, default=None):
ret = None if self.app is None else self.app.conf_value(path, default)
if ret is None and default is not None:
return default
return ret