30
30
# setting is True
31
31
import argparse
32
32
import cmd
33
+ import copy
33
34
import functools
34
35
import glob
35
36
import inspect
55
56
)
56
57
from typing import (
57
58
IO ,
59
+ TYPE_CHECKING ,
58
60
Any ,
59
61
Callable ,
60
62
Dict ,
99
101
HELP_FUNC_PREFIX ,
100
102
)
101
103
from .decorators import (
104
+ CommandParent ,
102
105
as_subcommand_to ,
103
106
with_argparser ,
104
107
)
@@ -197,6 +200,14 @@ def __init__(self) -> None:
197
200
DisabledCommand = namedtuple ('DisabledCommand' , ['command_function' , 'help_function' , 'completer_function' ])
198
201
199
202
203
+ if TYPE_CHECKING : # pragma: no cover
204
+ StaticArgParseBuilder = staticmethod [[], argparse .ArgumentParser ]
205
+ ClassArgParseBuilder = classmethod [Union ['Cmd' , CommandSet ], [], argparse .ArgumentParser ]
206
+ else :
207
+ StaticArgParseBuilder = staticmethod
208
+ ClassArgParseBuilder = classmethod
209
+
210
+
200
211
class Cmd (cmd .Cmd ):
201
212
"""An easy but powerful framework for writing line-oriented command interpreters.
202
213
@@ -510,6 +521,16 @@ def __init__(
510
521
# This does not affect self.formatted_completions.
511
522
self .matches_sorted = False
512
523
524
+ # Command parsers for this Cmd instance.
525
+ self ._command_parsers : Dict [str , argparse .ArgumentParser ] = {}
526
+
527
+ # Locates the command parser template or factory and creates an instance-specific parser
528
+ for command in self .get_all_commands ():
529
+ self ._register_command_parser (command , self .cmd_func (command )) # type: ignore[arg-type]
530
+
531
+ # Add functions decorated to be subcommands
532
+ self ._register_subcommands (self )
533
+
513
534
############################################################################################################
514
535
# The following code block loads CommandSets, verifies command names, and registers subcommands.
515
536
# This block should appear after all attributes have been created since the registration code
@@ -529,9 +550,6 @@ def __init__(
529
550
if not valid :
530
551
raise ValueError (f"Invalid command name '{ cur_cmd } ': { errmsg } " )
531
552
532
- # Add functions decorated to be subcommands
533
- self ._register_subcommands (self )
534
-
535
553
self .suggest_similar_command = suggest_similar_command
536
554
self .default_suggestion_message = "Did you mean {}?"
537
555
@@ -662,6 +680,48 @@ def register_command_set(self, cmdset: CommandSet) -> None:
662
680
cmdset .on_unregistered ()
663
681
raise
664
682
683
+ def _build_parser (
684
+ self ,
685
+ parent : CommandParent ,
686
+ parser_builder : Optional [
687
+ Union [argparse .ArgumentParser , Callable [[], argparse .ArgumentParser ], StaticArgParseBuilder , ClassArgParseBuilder ]
688
+ ],
689
+ ) -> Optional [argparse .ArgumentParser ]:
690
+ parser : Optional [argparse .ArgumentParser ] = None
691
+ if isinstance (parser_builder , staticmethod ):
692
+ parser = parser_builder .__func__ ()
693
+ elif isinstance (parser_builder , classmethod ):
694
+ parser = parser_builder .__func__ (parent if not None else self )
695
+ elif callable (parser_builder ):
696
+ parser = parser_builder ()
697
+ elif isinstance (parser_builder , argparse .ArgumentParser ):
698
+ if sys .version_info >= (3 , 6 , 4 ):
699
+ parser = copy .deepcopy (parser_builder )
700
+ else : # pragma: no cover
701
+ parser = parser_builder
702
+ return parser
703
+
704
+ def _register_command_parser (self , command : str , command_method : Callable [..., Any ]) -> None :
705
+ if command not in self ._command_parsers :
706
+ parser_builder = getattr (command_method , constants .CMD_ATTR_ARGPARSER , None )
707
+ parent = self .find_commandset_for_command (command ) or self
708
+ parser = self ._build_parser (parent , parser_builder )
709
+ if parser is None :
710
+ return
711
+
712
+ # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
713
+ from .decorators import (
714
+ _set_parser_prog ,
715
+ )
716
+
717
+ _set_parser_prog (parser , command )
718
+
719
+ # If the description has not been set, then use the method docstring if one exists
720
+ if parser .description is None and hasattr (command_method , '__wrapped__' ) and command_method .__wrapped__ .__doc__ :
721
+ parser .description = strip_doc_annotations (command_method .__wrapped__ .__doc__ )
722
+
723
+ self ._command_parsers [command ] = parser
724
+
665
725
def _install_command_function (self , command : str , command_wrapper : Callable [..., Any ], context : str = '' ) -> None :
666
726
cmd_func_name = COMMAND_FUNC_PREFIX + command
667
727
@@ -684,6 +744,8 @@ def _install_command_function(self, command: str, command_wrapper: Callable[...,
684
744
self .pwarning (f"Deleting macro '{ command } ' because it shares its name with a new command" )
685
745
del self .macros [command ]
686
746
747
+ self ._register_command_parser (command , command_wrapper )
748
+
687
749
setattr (self , cmd_func_name , command_wrapper )
688
750
689
751
def _install_completer_function (self , cmd_name : str , cmd_completer : CompleterFunc ) -> None :
@@ -730,6 +792,8 @@ def unregister_command_set(self, cmdset: CommandSet) -> None:
730
792
del self ._cmd_to_command_sets [cmd_name ]
731
793
732
794
delattr (self , COMMAND_FUNC_PREFIX + cmd_name )
795
+ if cmd_name in self ._command_parsers :
796
+ del self ._command_parsers [cmd_name ]
733
797
734
798
if hasattr (self , COMPLETER_FUNC_PREFIX + cmd_name ):
735
799
delattr (self , COMPLETER_FUNC_PREFIX + cmd_name )
@@ -749,14 +813,7 @@ def _check_uninstallable(self, cmdset: CommandSet) -> None:
749
813
750
814
for method in methods :
751
815
command_name = method [0 ][len (COMMAND_FUNC_PREFIX ) :]
752
-
753
- # Search for the base command function and verify it has an argparser defined
754
- if command_name in self .disabled_commands :
755
- command_func = self .disabled_commands [command_name ].command_function
756
- else :
757
- command_func = self .cmd_func (command_name )
758
-
759
- command_parser = cast (argparse .ArgumentParser , getattr (command_func , constants .CMD_ATTR_ARGPARSER , None ))
816
+ command_parser = self ._command_parsers .get (command_name , None )
760
817
761
818
def check_parser_uninstallable (parser : argparse .ArgumentParser ) -> None :
762
819
for action in parser ._actions :
@@ -795,7 +852,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
795
852
for method_name , method in methods :
796
853
subcommand_name : str = getattr (method , constants .SUBCMD_ATTR_NAME )
797
854
full_command_name : str = getattr (method , constants .SUBCMD_ATTR_COMMAND )
798
- subcmd_parser = getattr (method , constants .CMD_ATTR_ARGPARSER )
855
+ subcmd_parser_builder = getattr (method , constants .CMD_ATTR_ARGPARSER )
799
856
800
857
subcommand_valid , errmsg = self .statement_parser .is_valid_command (subcommand_name , is_subcommand = True )
801
858
if not subcommand_valid :
@@ -815,7 +872,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
815
872
raise CommandSetRegistrationError (
816
873
f"Could not find command '{ command_name } ' needed by subcommand: { str (method )} "
817
874
)
818
- command_parser = getattr ( command_func , constants . CMD_ATTR_ARGPARSER , None )
875
+ command_parser = self . _command_parsers . get ( command_name , None )
819
876
if command_parser is None :
820
877
raise CommandSetRegistrationError (
821
878
f"Could not find argparser for command '{ command_name } ' needed by subcommand: { str (method )} "
@@ -835,16 +892,17 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) ->
835
892
836
893
target_parser = find_subcommand (command_parser , subcommand_names )
837
894
895
+ subcmd_parser = cast (argparse .ArgumentParser , self ._build_parser (cmdset , subcmd_parser_builder ))
896
+ from .decorators import (
897
+ _set_parser_prog ,
898
+ )
899
+
900
+ _set_parser_prog (subcmd_parser , f'{ command_name } { subcommand_name } ' )
901
+ if subcmd_parser .description is None and method .__doc__ :
902
+ subcmd_parser .description = strip_doc_annotations (method .__doc__ )
903
+
838
904
for action in target_parser ._actions :
839
905
if isinstance (action , argparse ._SubParsersAction ):
840
- # Temporary workaround for avoiding subcommand help text repeatedly getting added to
841
- # action._choices_actions. Until we have instance-specific parser objects, we will remove
842
- # any existing subcommand which has the same name before replacing it. This problem is
843
- # exercised when more than one cmd2.Cmd-based object is created and the same subcommands
844
- # get added each time. Argparse overwrites the previous subcommand but keeps growing the help
845
- # text which is shown by running something like 'alias -h'.
846
- action .remove_parser (subcommand_name ) # type: ignore[arg-type,attr-defined]
847
-
848
906
# Get the kwargs for add_parser()
849
907
add_parser_kwargs = getattr (method , constants .SUBCMD_ATTR_ADD_PARSER_KWARGS , {})
850
908
@@ -916,7 +974,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
916
974
raise CommandSetRegistrationError (
917
975
f"Could not find command '{ command_name } ' needed by subcommand: { str (method )} "
918
976
)
919
- command_parser = getattr ( command_func , constants . CMD_ATTR_ARGPARSER , None )
977
+ command_parser = self . _command_parsers . get ( command_name , None )
920
978
if command_parser is None : # pragma: no cover
921
979
# This really shouldn't be possible since _register_subcommands would prevent this from happening
922
980
# but keeping in case it does for some strange reason
@@ -2037,7 +2095,7 @@ def _perform_completion(
2037
2095
else :
2038
2096
# There's no completer function, next see if the command uses argparse
2039
2097
func = self .cmd_func (command )
2040
- argparser : Optional [ argparse . ArgumentParser ] = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
2098
+ argparser = self . _command_parsers . get ( command , None )
2041
2099
2042
2100
if func is not None and argparser is not None :
2043
2101
# Get arguments for complete()
@@ -3272,14 +3330,19 @@ def _cmdloop(self) -> None:
3272
3330
#############################################################
3273
3331
3274
3332
# Top-level parser for alias
3275
- alias_description = "Manage aliases\n " "\n " "An alias is a command that enables replacement of a word by another string."
3276
- alias_epilog = "See also:\n " " macro"
3277
- alias_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = alias_description , epilog = alias_epilog )
3278
- alias_subparsers = alias_parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' )
3279
- alias_subparsers .required = True
3333
+ @staticmethod
3334
+ def _build_alias_parser () -> argparse .ArgumentParser :
3335
+ alias_description = (
3336
+ "Manage aliases\n " "\n " "An alias is a command that enables replacement of a word by another string."
3337
+ )
3338
+ alias_epilog = "See also:\n " " macro"
3339
+ alias_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = alias_description , epilog = alias_epilog )
3340
+ alias_subparsers = alias_parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' )
3341
+ alias_subparsers .required = True
3342
+ return alias_parser
3280
3343
3281
3344
# Preserve quotes since we are passing strings to other commands
3282
- @with_argparser (alias_parser , preserve_quotes = True )
3345
+ @with_argparser (_build_alias_parser , preserve_quotes = True )
3283
3346
def do_alias (self , args : argparse .Namespace ) -> None :
3284
3347
"""Manage aliases"""
3285
3348
# Call handler for whatever subcommand was selected
@@ -3694,7 +3757,7 @@ def complete_help_subcommands(
3694
3757
3695
3758
# Check if this command uses argparse
3696
3759
func = self .cmd_func (command )
3697
- argparser = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
3760
+ argparser = self . _command_parsers . get ( command , None )
3698
3761
if func is None or argparser is None :
3699
3762
return []
3700
3763
@@ -3730,7 +3793,7 @@ def do_help(self, args: argparse.Namespace) -> None:
3730
3793
# Getting help for a specific command
3731
3794
func = self .cmd_func (args .command )
3732
3795
help_func = getattr (self , constants .HELP_FUNC_PREFIX + args .command , None )
3733
- argparser = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
3796
+ argparser = self . _command_parsers . get ( args . command , None )
3734
3797
3735
3798
# If the command function uses argparse, then use argparse's help
3736
3799
if func is not None and argparser is not None :
@@ -3866,7 +3929,7 @@ def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str
3866
3929
help_topics .remove (command )
3867
3930
3868
3931
# Non-argparse commands can have help_functions for their documentation
3869
- if not hasattr ( func , constants . CMD_ATTR_ARGPARSER ) :
3932
+ if command not in self . _command_parsers :
3870
3933
has_help_func = True
3871
3934
3872
3935
if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
@@ -3912,7 +3975,7 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
3912
3975
doc : Optional [str ]
3913
3976
3914
3977
# Non-argparse commands can have help_functions for their documentation
3915
- if not hasattr ( cmd_func , constants . CMD_ATTR_ARGPARSER ) and command in topics :
3978
+ if command not in self . _command_parsers and command in topics :
3916
3979
help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
3917
3980
result = io .StringIO ()
3918
3981
0 commit comments