@@ -494,6 +494,52 @@ def _ActionsContainer_add_argument( # noqa: N802
494494# Overwrite _ActionsContainer.add_argument with our patch
495495argparse ._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
498544class 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