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 <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-06-18 10:15:08 +02:00
commit effda76f33
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
3 changed files with 38 additions and 8 deletions

View file

@ -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

View file

@ -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,14 +89,24 @@ 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]
add_cmds_to_parser(sc.cmd, sc.parser, sc.cmd.children, all = all)
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

View file

@ -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