Skip to content

Commit 74a2889

Browse files
authored
Made more of the subcommand management API public. (#1643)
1 parent 14d91a7 commit 74a2889

9 files changed

Lines changed: 331 additions & 129 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ prompt is displayed.
9696
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()` and
9797
`set_theme()` functions to support lazy initialization and safer in-place updates of the
9898
theme.
99+
- Renamed `Cmd._command_parsers` to `Cmd.command_parsers`.
99100
- Enhancements
100101
- New `cmd2.Cmd` parameters
101102
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These

cmd2/argparse_utils.py

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,52 @@ def _ActionsContainer_add_argument( # noqa: N802
494494
# Overwrite _ActionsContainer.add_argument with our patch
495495
argparse._ActionsContainer.add_argument = _ActionsContainer_add_argument # type: ignore[method-assign]
496496

497+
############################################################################################################
498+
# Patch argparse._SubParsersAction by adding remove_parser() function
499+
############################################################################################################
500+
501+
502+
def _SubParsersAction_remove_parser( # noqa: N802
503+
self: argparse._SubParsersAction, # type: ignore[type-arg]
504+
name: str,
505+
) -> argparse.ArgumentParser:
506+
"""Remove a subparser from a subparsers group.
507+
508+
This function is added by cmd2 as a method called ``remove_parser()``
509+
to ``argparse._SubParsersAction`` class.
510+
511+
To call: ``action.remove_parser(name)``
512+
513+
:param self: instance of the _SubParsersAction being edited
514+
:param name: name of the subcommand for the subparser to remove
515+
:return: the removed parser
516+
:raises ValueError: if the subcommand doesn't exist
517+
"""
518+
if name not in self._name_parser_map:
519+
raise ValueError(f"Subcommand '{name}' not found")
520+
521+
subparser = self._name_parser_map[name]
522+
523+
# Find all names (primary and aliases) that map to this subparser
524+
all_names = [cur_name for cur_name, cur_parser in self._name_parser_map.items() if cur_parser is subparser]
525+
526+
# Remove the help entry for this subparser. To handle the case where
527+
# name is an alias, we remove the action whose 'dest' matches any of
528+
# the names mapped to this subparser.
529+
for choice_action in self._choices_actions:
530+
if choice_action.dest in all_names:
531+
self._choices_actions.remove(choice_action)
532+
break
533+
534+
# Remove all references to this subparser, including aliases.
535+
for cur_name in all_names:
536+
del self._name_parser_map[cur_name]
537+
538+
return cast(argparse.ArgumentParser, subparser)
539+
540+
541+
argparse._SubParsersAction.remove_parser = _SubParsersAction_remove_parser # type: ignore[attr-defined]
542+
497543

498544
class Cmd2ArgumentParser(argparse.ArgumentParser):
499545
"""Custom ArgumentParser class that improves error and help output."""
@@ -556,7 +602,7 @@ def __init__(
556602
self.description: RenderableType | None # type: ignore[assignment]
557603
self.epilog: RenderableType | None # type: ignore[assignment]
558604

559-
def _get_subparsers_action(self) -> "argparse._SubParsersAction[Cmd2ArgumentParser]":
605+
def get_subparsers_action(self) -> "argparse._SubParsersAction[Cmd2ArgumentParser]":
560606
"""Get the _SubParsersAction for this parser if it exists.
561607
562608
:return: the _SubParsersAction for this parser
@@ -619,7 +665,7 @@ def update_prog(self, prog: str) -> None:
619665
self.prog = prog
620666

621667
try:
622-
subparsers_action = self._get_subparsers_action()
668+
subparsers_action = self.get_subparsers_action()
623669
except ValueError:
624670
# This parser has no subcommands
625671
return
@@ -651,7 +697,7 @@ def update_prog(self, prog: str) -> None:
651697
subcmd_parser.update_prog(subcmd_prog)
652698
updated_parsers.add(subcmd_parser)
653699

654-
def _find_parser(self, subcommand_path: Iterable[str]) -> "Cmd2ArgumentParser":
700+
def find_parser(self, subcommand_path: Iterable[str]) -> "Cmd2ArgumentParser":
655701
"""Find a parser in the hierarchy based on a sequence of subcommand names.
656702
657703
:param subcommand_path: sequence of subcommand names leading to the target parser
@@ -660,7 +706,7 @@ def _find_parser(self, subcommand_path: Iterable[str]) -> "Cmd2ArgumentParser":
660706
"""
661707
parser = self
662708
for name in subcommand_path:
663-
subparsers_action = parser._get_subparsers_action()
709+
subparsers_action = parser.get_subparsers_action()
664710
if name not in subparsers_action.choices:
665711
raise ValueError(f"Subcommand '{name}' not found in '{parser.prog}'")
666712
parser = subparsers_action.choices[name]
@@ -691,8 +737,8 @@ def attach_subcommand(
691737
f"Received: '{type(subcommand_parser).__name__}'."
692738
)
693739

694-
target_parser = self._find_parser(subcommand_path)
695-
subparsers_action = target_parser._get_subparsers_action()
740+
target_parser = self.find_parser(subcommand_path)
741+
subparsers_action = target_parser.get_subparsers_action()
696742

697743
# Verify the parser is compatible with the 'parser_class' configured for this
698744
# subcommand group. We use isinstance() here to allow for subclasses, providing
@@ -728,28 +774,16 @@ def detach_subcommand(self, subcommand_path: Iterable[str], subcommand: str) ->
728774
:return: the detached parser
729775
:raises ValueError: if the command path is invalid or the subcommand doesn't exist
730776
"""
731-
target_parser = self._find_parser(subcommand_path)
732-
subparsers_action = target_parser._get_subparsers_action()
733-
734-
subparser = subparsers_action._name_parser_map.get(subcommand)
735-
if subparser is None:
736-
raise ValueError(f"Subcommand '{subcommand}' not found in '{target_parser.prog}'")
737-
738-
# Remove this subcommand and all its aliases from the base command
739-
to_remove = []
740-
for cur_name, cur_parser in subparsers_action._name_parser_map.items():
741-
if cur_parser is subparser:
742-
to_remove.append(cur_name)
743-
for cur_name in to_remove:
744-
del subparsers_action._name_parser_map[cur_name]
745-
746-
# Remove this subcommand from its base command's help text
747-
for choice_action in subparsers_action._choices_actions:
748-
if choice_action.dest == subcommand:
749-
subparsers_action._choices_actions.remove(choice_action)
750-
break
777+
target_parser = self.find_parser(subcommand_path)
778+
subparsers_action = target_parser.get_subparsers_action()
751779

752-
return subparser
780+
try:
781+
return cast(
782+
Cmd2ArgumentParser,
783+
subparsers_action.remove_parser(subcommand), # type: ignore[attr-defined]
784+
)
785+
except ValueError:
786+
raise ValueError(f"Subcommand '{subcommand}' not found in '{target_parser.prog}'") from None
753787

754788
def error(self, message: str) -> NoReturn:
755789
"""Override that applies custom formatting to the error message."""

0 commit comments

Comments
 (0)