From 3b0f7727a7b696022dbce4eecdcee442d80d367a Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Thu, 18 Jun 2026 10:15:08 +0200 Subject: [PATCH] lib.Cmd.AbstractCmd.aliases: Add property Add a property aliases to AbstractCmd in prepeparation for commands to bear multiple names / abbreviations / aliases. The App.add_cmds_to_parser() function uses parse_known_args() to determine which subcommand was invoked, then conditionally registers nested subcommands. The lookup dictionary (scs) contains only canonical names, not aliases, so add them too, otherwise using the alias instead of the canonical name causes the lookup to fail and nested subcommands to never be registered. Fix: Register each alias in scs pointing to the same SubCommand object, and deduplicate with id(sc) when iterating in all=True mode to avoid infinite recursion on help output. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/cmds/Cmd.py | 11 +++++++++-- src/python/jw/pkg/lib/App.py | 15 ++++++++++++--- src/python/jw/pkg/lib/Cmd.py | 16 ++++++++++++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/python/jw/pkg/cmds/Cmd.py b/src/python/jw/pkg/cmds/Cmd.py index ecd90de8..7fbb7e21 100644 --- a/src/python/jw/pkg/cmds/Cmd.py +++ b/src/python/jw/pkg/cmds/Cmd.py @@ -6,12 +6,19 @@ from ..App import App as Parent from ..CmdBase import CmdBase as Base if TYPE_CHECKING: + from typing import Iterable from ..lib.Distro import Distro class Cmd(Base): # export - def __init__(self, parent: Parent, name: str, help: str) -> None: - super().__init__(parent, name, help) + def __init__( + self, + parent: Parent, + name: str, + help: str, + aliases: Iterable[str] | None = None + ) -> None: + super().__init__(parent, name, help, aliases = aliases) async def _run(self, args): # Missing subcommand diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index 343dc5b0..c9fab8e0 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -56,6 +56,7 @@ class App: # export cmd.name, help = cmd.help, description = cmd.description, + aliases = cmd.aliases, formatter_class = ArgumentDefaultsHelpFormatter, ) parser.set_defaults(func = cmd.run) @@ -88,13 +89,21 @@ class App: # export for cmd in cmds: cmd.set_parent(parent) scs[cmd.name] = SubCommand(cmd, add_cmd_to_parser(cmd, subparsers)) + for alias in cmd.aliases: + scs[alias] = scs[cmd.name] if all: + seen: set[int] = set() for sc in scs.values(): - add_cmds_to_parser(sc.cmd, sc.parser, sc.cmd.children, all = all) + if id(sc) not in seen: + seen.add(id(sc)) + add_cmds_to_parser( + sc.cmd, sc.parser, sc.cmd.children, all = all + ) return args, unknown = self.__parser.parse_known_args() - if args.command in scs: - sc = scs[args.command] + cmd_name = getattr(args, 'command', None) + if cmd_name in scs: + sc = scs[cmd_name] add_cmds_to_parser(sc.cmd, sc.parser, sc.cmd.children, all = all) from .Cmd import AbstractCmd diff --git a/src/python/jw/pkg/lib/Cmd.py b/src/python/jw/pkg/lib/Cmd.py index 91beb310..9a3caa39 100644 --- a/src/python/jw/pkg/lib/Cmd.py +++ b/src/python/jw/pkg/lib/Cmd.py @@ -10,7 +10,7 @@ from .Types import LoadTypes, Types if TYPE_CHECKING: from argparse import ArgumentParser - from typing import Any + from typing import Any, Iterable from .App import App @@ -150,6 +150,13 @@ class AbstractCmd(abc.ABC): def description(self) -> str: return self._description() + def _aliases(self) -> Iterable[str]: + return [] + + @property + def aliases(self) -> Iterable[str]: + return self._aliases() + class Cmd(AbstractCmd): # export def __init__( @@ -157,12 +164,14 @@ class Cmd(AbstractCmd): # export parent: App | Cmd, name: str, help: str, - description: str | None = None + description: str | None = None, + aliases: Iterable[str] | None = None ) -> None: super().__init__(parent) self.__name = name self.__help = help self.__description = description if description else help + self.__aliases = aliases if aliases else [] def _name(self) -> str: return self.__name @@ -172,3 +181,6 @@ class Cmd(AbstractCmd): # export def _description(self) -> str: return self.__description + + def _aliases(self) -> Iterable[str]: + return self.__aliases