3030# setting is True
3131import argparse
3232import cmd
33+ import copy
3334import functools
3435import glob
3536import inspect
140141from .utils import (
141142 Settable ,
142143 get_defining_class ,
144+ is_callable ,
143145 strip_doc_annotations ,
144146 suggest_similar ,
145147)
@@ -510,6 +512,16 @@ def __init__(
510512 # This does not affect self.formatted_completions.
511513 self .matches_sorted = False
512514
515+ # Command parsers for this Cmd instance.
516+ self ._command_parsers : Dict [str , argparse .ArgumentParser ] = {}
517+
518+ # Locates the command parser template or factory and creates an instance-specific parser
519+ for command in self .get_all_commands ():
520+ self ._register_command_parser (command , self .cmd_func (command )) # type: ignore[arg-type]
521+
522+ # Add functions decorated to be subcommands
523+ self ._register_subcommands (self )
524+
513525 ############################################################################################################
514526 # The following code block loads CommandSets, verifies command names, and registers subcommands.
515527 # This block should appear after all attributes have been created since the registration code
@@ -529,9 +541,6 @@ def __init__(
529541 if not valid :
530542 raise ValueError (f"Invalid command name '{ cur_cmd } ': { errmsg } " )
531543
532- # Add functions decorated to be subcommands
533- self ._register_subcommands (self )
534-
535544 self .suggest_similar_command = suggest_similar_command
536545 self .default_suggestion_message = "Did you mean {}?"
537546
@@ -659,6 +668,43 @@ def register_command_set(self, cmdset: CommandSet) -> None:
659668 cmdset .on_unregistered ()
660669 raise
661670
671+ def _build_parser (self , parent : Union ['Cmd' , CommandSet ], parser_builder : Any ) -> Optional [argparse .ArgumentParser ]:
672+ parser : Optional [argparse .ArgumentParser ] = None
673+ if is_callable (parser_builder ):
674+ if isinstance (parser_builder , staticmethod ):
675+ parser = cast (argparse .ArgumentParser , parser_builder .__func__ ())
676+ elif isinstance (parser_builder , classmethod ):
677+ parser = cast (argparse .ArgumentParser , parser_builder .__func__ (parent if not None else self ))
678+ else :
679+ parser = cast (argparse .ArgumentParser , parser_builder ()) # type: ignore[misc]
680+ elif isinstance (parser_builder , argparse .ArgumentParser ):
681+ if sys .version_info >= (3 , 6 , 4 ):
682+ parser = copy .deepcopy (parser_builder )
683+ else : # pragma: no cover
684+ parser = parser_builder
685+ return parser
686+
687+ def _register_command_parser (self , command : str , command_method : Callable [..., Any ]) -> None :
688+ if command not in self ._command_parsers :
689+ parser_builder = getattr (command_method , constants .CMD_ATTR_ARGPARSER , None )
690+ parent = self .find_commandset_for_command (command ) or self
691+ parser = self ._build_parser (parent , parser_builder )
692+ if parser is None :
693+ return
694+
695+ # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
696+ from .decorators import (
697+ _set_parser_prog ,
698+ )
699+
700+ _set_parser_prog (parser , command )
701+
702+ # If the description has not been set, then use the method docstring if one exists
703+ if parser .description is None and hasattr (command_method , '__wrapped__' ) and command_method .__wrapped__ .__doc__ :
704+ parser .description = strip_doc_annotations (command_method .__wrapped__ .__doc__ )
705+
706+ self ._command_parsers [command ] = parser
707+
662708 def _install_command_function (self , command : str , command_wrapper : Callable [..., Any ], context : str = '' ) -> None :
663709 cmd_func_name = COMMAND_FUNC_PREFIX + command
664710
@@ -681,6 +727,8 @@ def _install_command_function(self, command: str, command_wrapper: Callable[...,
681727 self .pwarning (f"Deleting macro '{ command } ' because it shares its name with a new command" )
682728 del self .macros [command ]
683729
730+ self ._register_command_parser (command , command_wrapper )
731+
684732 setattr (self , cmd_func_name , command_wrapper )
685733
686734 def _install_completer_function (self , cmd_name : str , cmd_completer : CompleterFunc ) -> None :
@@ -727,6 +775,8 @@ def unregister_command_set(self, cmdset: CommandSet) -> None:
727775 del self ._cmd_to_command_sets [cmd_name ]
728776
729777 delattr (self , COMMAND_FUNC_PREFIX + cmd_name )
778+ if cmd_name in self ._command_parsers :
779+ del self ._command_parsers [cmd_name ]
730780
731781 if hasattr (self , COMPLETER_FUNC_PREFIX + cmd_name ):
732782 delattr (self , COMPLETER_FUNC_PREFIX + cmd_name )
@@ -746,14 +796,7 @@ def _check_uninstallable(self, cmdset: CommandSet) -> None:
746796
747797 for method in methods :
748798 command_name = method [0 ][len (COMMAND_FUNC_PREFIX ) :]
749-
750- # Search for the base command function and verify it has an argparser defined
751- if command_name in self .disabled_commands :
752- command_func = self .disabled_commands [command_name ].command_function
753- else :
754- command_func = self .cmd_func (command_name )
755-
756- command_parser = cast (argparse .ArgumentParser , getattr (command_func , constants .CMD_ATTR_ARGPARSER , None ))
799+ command_parser = self ._command_parsers .get (command_name , None )
757800
758801 def check_parser_uninstallable (parser : argparse .ArgumentParser ) -> None :
759802 for action in parser ._actions :
@@ -792,7 +835,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
792835 for method_name , method in methods :
793836 subcommand_name : str = getattr (method , constants .SUBCMD_ATTR_NAME )
794837 full_command_name : str = getattr (method , constants .SUBCMD_ATTR_COMMAND )
795- subcmd_parser = getattr (method , constants .CMD_ATTR_ARGPARSER )
838+ subcmd_parser_builder = getattr (method , constants .CMD_ATTR_ARGPARSER )
796839
797840 subcommand_valid , errmsg = self .statement_parser .is_valid_command (subcommand_name , is_subcommand = True )
798841 if not subcommand_valid :
@@ -812,7 +855,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
812855 raise CommandSetRegistrationError (
813856 f"Could not find command '{ command_name } ' needed by subcommand: { str (method )} "
814857 )
815- command_parser = getattr ( command_func , constants . CMD_ATTR_ARGPARSER , None )
858+ command_parser = self . _command_parsers . get ( command_name , None )
816859 if command_parser is None :
817860 raise CommandSetRegistrationError (
818861 f"Could not find argparser for command '{ command_name } ' needed by subcommand: { str (method )} "
@@ -832,16 +875,17 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) ->
832875
833876 target_parser = find_subcommand (command_parser , subcommand_names )
834877
878+ subcmd_parser = cast (argparse .ArgumentParser , self ._build_parser (cmdset , subcmd_parser_builder ))
879+ from .decorators import (
880+ _set_parser_prog ,
881+ )
882+
883+ _set_parser_prog (subcmd_parser , f'{ command_name } { subcommand_name } ' )
884+ if subcmd_parser .description is None and method .__doc__ :
885+ subcmd_parser .description = strip_doc_annotations (method .__doc__ )
886+
835887 for action in target_parser ._actions :
836888 if isinstance (action , argparse ._SubParsersAction ):
837- # Temporary workaround for avoiding subcommand help text repeatedly getting added to
838- # action._choices_actions. Until we have instance-specific parser objects, we will remove
839- # any existing subcommand which has the same name before replacing it. This problem is
840- # exercised when more than one cmd2.Cmd-based object is created and the same subcommands
841- # get added each time. Argparse overwrites the previous subcommand but keeps growing the help
842- # text which is shown by running something like 'alias -h'.
843- action .remove_parser (subcommand_name ) # type: ignore[arg-type,attr-defined]
844-
845889 # Get the kwargs for add_parser()
846890 add_parser_kwargs = getattr (method , constants .SUBCMD_ATTR_ADD_PARSER_KWARGS , {})
847891
@@ -913,7 +957,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
913957 raise CommandSetRegistrationError (
914958 f"Could not find command '{ command_name } ' needed by subcommand: { str (method )} "
915959 )
916- command_parser = getattr ( command_func , constants . CMD_ATTR_ARGPARSER , None )
960+ command_parser = self . _command_parsers . get ( command_name , None )
917961 if command_parser is None : # pragma: no cover
918962 # This really shouldn't be possible since _register_subcommands would prevent this from happening
919963 # but keeping in case it does for some strange reason
@@ -2034,7 +2078,7 @@ def _perform_completion(
20342078 else :
20352079 # There's no completer function, next see if the command uses argparse
20362080 func = self .cmd_func (command )
2037- argparser : Optional [ argparse . ArgumentParser ] = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
2081+ argparser = self . _command_parsers . get ( command , None )
20382082
20392083 if func is not None and argparser is not None :
20402084 # Get arguments for complete()
@@ -3259,14 +3303,19 @@ def _cmdloop(self) -> None:
32593303 #############################################################
32603304
32613305 # Top-level parser for alias
3262- alias_description = "Manage aliases\n " "\n " "An alias is a command that enables replacement of a word by another string."
3263- alias_epilog = "See also:\n " " macro"
3264- alias_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = alias_description , epilog = alias_epilog )
3265- alias_subparsers = alias_parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' )
3266- alias_subparsers .required = True
3306+ @staticmethod
3307+ def _build_alias_parser () -> argparse .ArgumentParser :
3308+ alias_description = (
3309+ "Manage aliases\n " "\n " "An alias is a command that enables replacement of a word by another string."
3310+ )
3311+ alias_epilog = "See also:\n " " macro"
3312+ alias_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = alias_description , epilog = alias_epilog )
3313+ alias_subparsers = alias_parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' )
3314+ alias_subparsers .required = True
3315+ return alias_parser
32673316
32683317 # Preserve quotes since we are passing strings to other commands
3269- @with_argparser (alias_parser , preserve_quotes = True )
3318+ @with_argparser (_build_alias_parser , preserve_quotes = True )
32703319 def do_alias (self , args : argparse .Namespace ) -> None :
32713320 """Manage aliases"""
32723321 # Call handler for whatever subcommand was selected
@@ -3681,7 +3730,7 @@ def complete_help_subcommands(
36813730
36823731 # Check if this command uses argparse
36833732 func = self .cmd_func (command )
3684- argparser = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
3733+ argparser = self . _command_parsers . get ( command , None )
36853734 if func is None or argparser is None :
36863735 return []
36873736
@@ -3717,7 +3766,7 @@ def do_help(self, args: argparse.Namespace) -> None:
37173766 # Getting help for a specific command
37183767 func = self .cmd_func (args .command )
37193768 help_func = getattr (self , constants .HELP_FUNC_PREFIX + args .command , None )
3720- argparser = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
3769+ argparser = self . _command_parsers . get ( args . command , None )
37213770
37223771 # If the command function uses argparse, then use argparse's help
37233772 if func is not None and argparser is not None :
@@ -3853,7 +3902,7 @@ def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str
38533902 help_topics .remove (command )
38543903
38553904 # Non-argparse commands can have help_functions for their documentation
3856- if not hasattr ( func , constants . CMD_ATTR_ARGPARSER ) :
3905+ if command not in self . _command_parsers :
38573906 has_help_func = True
38583907
38593908 if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
@@ -3899,7 +3948,7 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
38993948 doc : Optional [str ]
39003949
39013950 # Non-argparse commands can have help_functions for their documentation
3902- if not hasattr ( cmd_func , constants . CMD_ATTR_ARGPARSER ) and command in topics :
3951+ if command not in self . _command_parsers and command in topics :
39033952 help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
39043953 result = io .StringIO ()
39053954
0 commit comments