@@ -222,19 +222,6 @@ def get_choices(self) -> Choices:
222222
223223- ``action.get_<name>()``
224224- ``action.set_<name>(value)``
225-
226- **Subcommand Manipulation**
227-
228- cmd2 has patched ``argparse._SubParsersAction`` with new functions to better facilitate the
229- addition and removal of subcommand parsers.
230-
231- ``argparse._SubParsersAction.attach_parser`` - new function to attach
232- an existing ArgumentParser to a subparsers action. See ``_SubParsersAction_attach_parser``
233- for more details.
234-
235- ``argparse._SubParsersAction.detach_parser`` - new function to detach a
236- parser from a subparsers action. See ``_SubParsersAction_detach_parser`` for
237- more details.
238225"""
239226
240227import argparse
@@ -522,87 +509,6 @@ def _ActionsContainer_add_argument( # noqa: N802
522509setattr (argparse ._ActionsContainer , 'add_argument' , _ActionsContainer_add_argument )
523510
524511
525- ############################################################################################################
526- # Patch argparse._SubParsersAction to add attach_parser function
527- ############################################################################################################
528-
529-
530- def _SubParsersAction_attach_parser ( # noqa: N802
531- self : argparse ._SubParsersAction , # type: ignore[type-arg]
532- name : str ,
533- subcmd_parser : argparse .ArgumentParser ,
534- ** add_parser_kwargs : Any ,
535- ) -> None :
536- """Attach an existing parser to a subparsers action.
537-
538- This is useful when a parser is pre-configured (e.g. by cmd2's subcommand decorator)
539- and needs to be attached to a parent parser.
540-
541- This function is added by cmd2 as a method called ``attach_parser()``
542- to ``argparse._SubParsersAction`` class.
543-
544- To call: ``action.attach_parser(name, subcmd_parser, **add_parser_kwargs)``
545-
546- :param self: instance of the _SubParsersAction being edited
547- :param name: name of the subcommand to add
548- :param subcmd_parser: the parser for this new subcommand
549- :param add_parser_kwargs: registration-specific kwargs for add_parser()
550- (e.g. help, aliases, deprecated [Python 3.13+])
551- """
552- # Use add_parser to register the subcommand name and any aliases
553- self .add_parser (name , ** add_parser_kwargs )
554-
555- # Replace the parser created by add_parser() with our pre-configured one
556- self ._name_parser_map [name ] = subcmd_parser
557-
558- # Remap any aliases to our pre-configured parser
559- for alias in add_parser_kwargs .get ("aliases" , ()):
560- self ._name_parser_map [alias ] = subcmd_parser
561-
562-
563- setattr (argparse ._SubParsersAction , 'attach_parser' , _SubParsersAction_attach_parser )
564-
565- ############################################################################################################
566- # Patch argparse._SubParsersAction to add detach_parser function
567- ############################################################################################################
568-
569-
570- def _SubParsersAction_detach_parser ( # noqa: N802
571- self : argparse ._SubParsersAction , # type: ignore[type-arg]
572- name : str ,
573- ) -> argparse .ArgumentParser | None :
574- """Detach a parser from a subparsers action and return it.
575-
576- This function is added by cmd2 as a method called ``detach_parser()`` to ``argparse._SubParsersAction`` class.
577-
578- To call: ``action.detach_parser(name)``
579-
580- :param self: instance of the _SubParsersAction being edited
581- :param name: name of the subcommand for the parser to detach
582- :return: the parser which was detached or None if the subcommand doesn't exist
583- """
584- subparser = self ._name_parser_map .get (name )
585-
586- if subparser is not None :
587- # Remove this subcommand and all its aliases from the base command
588- to_remove = []
589- for cur_name , cur_parser in self ._name_parser_map .items ():
590- if cur_parser is subparser :
591- to_remove .append (cur_name )
592- for cur_name in to_remove :
593- del self ._name_parser_map [cur_name ]
594-
595- # Remove this subcommand from its base command's help text
596- for choice_action in self ._choices_actions :
597- if choice_action .dest == name :
598- self ._choices_actions .remove (choice_action )
599- break
600-
601- return subparser
602-
603-
604- setattr (argparse ._SubParsersAction , 'detach_parser' , _SubParsersAction_detach_parser )
605-
606512############################################################################################################
607513# Unless otherwise noted, everything below this point are copied from Python's
608514# argparse implementation with minor tweaks to adjust output.
@@ -865,20 +771,26 @@ def __init__(
865771
866772 self .ap_completer_type = ap_completer_type
867773
868- def add_subparsers (self , ** kwargs : Any ) -> argparse ._SubParsersAction : # type: ignore[type-arg]
869- """Add a subcommand parser.
774+ def add_subparsers ( # type: ignore[override]
775+ self ,
776+ ** kwargs : Any ,
777+ ) -> "argparse._SubParsersAction[Cmd2ArgumentParser]" :
778+ """Override for improved defaults and type safety.
870779
871- Set a default title if one was not given.
780+ This override does two things.
781+ 1. Sets a default title if one was not given.
782+ 2. Narrows the return type to provide better IDE autocompletion
783+ and type safety for `Cmd2ArgumentParser` instances.
872784
873785 :param kwargs: additional keyword arguments
874- :return: argparse Subparser Action
786+ :return: _SubParsersAction which stores Cmd2ArgumentParsers
875787 """
876788 if 'title' not in kwargs :
877789 kwargs ['title' ] = 'subcommands'
878790
879791 return super ().add_subparsers (** kwargs )
880792
881- def _get_subparsers_action (self ) -> argparse ._SubParsersAction : # type: ignore[type-arg]
793+ def _get_subparsers_action (self ) -> " argparse._SubParsersAction[Cmd2ArgumentParser]" :
882794 """Get the _SubParsersAction for this parser if it exists.
883795
884796 :return: the _SubParsersAction for this parser
@@ -951,7 +863,7 @@ def _find_parser(self, subcommand_path: Iterable[str]) -> 'Cmd2ArgumentParser':
951863 subparsers_action = parser ._get_subparsers_action ()
952864 if name not in subparsers_action .choices :
953865 raise ValueError (f"Subcommand '{ name } ' not found in '{ parser .prog } '" )
954- parser = cast ( Cmd2ArgumentParser , subparsers_action .choices [name ])
866+ parser = subparsers_action .choices [name ]
955867 return parser
956868
957869 def attach_subcommand (
@@ -972,7 +884,19 @@ def attach_subcommand(
972884 """
973885 target_parser = self ._find_parser (subcommand_path )
974886 subparsers_action = target_parser ._get_subparsers_action ()
975- subparsers_action .attach_parser (subcommand , parser , ** add_parser_kwargs ) # type: ignore[attr-defined]
887+
888+ # Use add_parser to register the subcommand name and any aliases
889+ new_parser = subparsers_action .add_parser (subcommand , ** add_parser_kwargs )
890+
891+ # Ensure the parser and any nested subparsers have the correct 'prog' value.
892+ parser .update_prog (new_parser .prog )
893+
894+ # Replace the parser created by add_parser() with our pre-configured one
895+ subparsers_action ._name_parser_map [subcommand ] = parser
896+
897+ # Remap any aliases to our pre-configured parser
898+ for alias in add_parser_kwargs .get ("aliases" , ()):
899+ subparsers_action ._name_parser_map [alias ] = parser
976900
977901 def detach_subcommand (self , subcommand_path : Iterable [str ], subcommand : str ) -> 'Cmd2ArgumentParser' :
978902 """Detach a subcommand from a command at the specified path.
@@ -985,10 +909,26 @@ def detach_subcommand(self, subcommand_path: Iterable[str], subcommand: str) ->
985909 """
986910 target_parser = self ._find_parser (subcommand_path )
987911 subparsers_action = target_parser ._get_subparsers_action ()
988- detached = subparsers_action .detach_parser (subcommand ) # type: ignore[attr-defined]
989- if detached is None :
912+
913+ subparser = subparsers_action ._name_parser_map .get (subcommand )
914+ if subparser is None :
990915 raise ValueError (f"Subcommand '{ subcommand } ' not found in '{ target_parser .prog } '" )
991- return cast (Cmd2ArgumentParser , detached )
916+
917+ # Remove this subcommand and all its aliases from the base command
918+ to_remove = []
919+ for cur_name , cur_parser in subparsers_action ._name_parser_map .items ():
920+ if cur_parser is subparser :
921+ to_remove .append (cur_name )
922+ for cur_name in to_remove :
923+ del subparsers_action ._name_parser_map [cur_name ]
924+
925+ # Remove this subcommand from its base command's help text
926+ for choice_action in subparsers_action ._choices_actions :
927+ if choice_action .dest == subcommand :
928+ subparsers_action ._choices_actions .remove (choice_action )
929+ break
930+
931+ return subparser
992932
993933 def error (self , message : str ) -> NoReturn :
994934 """Override that applies custom formatting to the error message."""
0 commit comments