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
69
70
TypeVar ,
70
71
Union ,
71
72
cast ,
73
+ TYPE_CHECKING ,
72
74
)
73
75
74
76
from . import (
100
102
)
101
103
from .decorators import (
102
104
as_subcommand_to ,
103
- with_argparser ,
105
+ with_argparser , CommandParent ,
104
106
)
105
107
from .exceptions import (
106
108
Cmd2ShlexError ,
@@ -197,6 +199,14 @@ def __init__(self) -> None:
197
199
DisabledCommand = namedtuple ('DisabledCommand' , ['command_function' , 'help_function' , 'completer_function' ])
198
200
199
201
202
+ if TYPE_CHECKING : # pragma: no cover
203
+ StaticArgParseBuilder = staticmethod [[], argparse .ArgumentParser ]
204
+ ClassArgParseBuilder = classmethod [Union ['Cmd' , CommandSet ], [], argparse .ArgumentParser ]
205
+ else :
206
+ StaticArgParseBuilder = staticmethod
207
+ ClassArgParseBuilder = classmethod
208
+
209
+
200
210
class Cmd (cmd .Cmd ):
201
211
"""An easy but powerful framework for writing line-oriented command interpreters.
202
212
@@ -510,6 +520,16 @@ def __init__(
510
520
# This does not affect self.formatted_completions.
511
521
self .matches_sorted = False
512
522
523
+ # Command parsers for this Cmd instance.
524
+ self ._command_parsers : Dict [str , argparse .ArgumentParser ] = {}
525
+
526
+ # Locates the command parser template or factory and creates an instance-specific parser
527
+ for command in self .get_all_commands ():
528
+ self ._register_command_parser (command , self .cmd_func (command )) # type: ignore[arg-type]
529
+
530
+ # Add functions decorated to be subcommands
531
+ self ._register_subcommands (self )
532
+
513
533
############################################################################################################
514
534
# The following code block loads CommandSets, verifies command names, and registers subcommands.
515
535
# This block should appear after all attributes have been created since the registration code
@@ -529,9 +549,6 @@ def __init__(
529
549
if not valid :
530
550
raise ValueError (f"Invalid command name '{ cur_cmd } ': { errmsg } " )
531
551
532
- # Add functions decorated to be subcommands
533
- self ._register_subcommands (self )
534
-
535
552
self .suggest_similar_command = suggest_similar_command
536
553
self .default_suggestion_message = "Did you mean {}?"
537
554
@@ -659,6 +676,48 @@ def register_command_set(self, cmdset: CommandSet) -> None:
659
676
cmdset .on_unregistered ()
660
677
raise
661
678
679
+ def _build_parser (self ,
680
+ parent : CommandParent ,
681
+ parser_builder : Optional [Union [argparse .ArgumentParser ,
682
+ Callable [[], argparse .ArgumentParser ],
683
+ StaticArgParseBuilder ,
684
+ ClassArgParseBuilder ]]
685
+ ) -> Optional [argparse .ArgumentParser ]:
686
+ parser : Optional [argparse .ArgumentParser ] = None
687
+ if isinstance (parser_builder , staticmethod ):
688
+ parser = parser_builder .__func__ ()
689
+ elif isinstance (parser_builder , classmethod ):
690
+ parser = parser_builder .__func__ (parent if not None else self )
691
+ elif callable (parser_builder ):
692
+ parser = parser_builder ()
693
+ elif isinstance (parser_builder , argparse .ArgumentParser ):
694
+ if sys .version_info >= (3 , 6 , 4 ):
695
+ parser = copy .deepcopy (parser_builder )
696
+ else : # pragma: no cover
697
+ parser = parser_builder
698
+ return parser
699
+
700
+ def _register_command_parser (self , command : str , command_method : Callable [..., Any ]) -> None :
701
+ if command not in self ._command_parsers :
702
+ parser_builder = getattr (command_method , constants .CMD_ATTR_ARGPARSER , None )
703
+ parent = self .find_commandset_for_command (command ) or self
704
+ parser = self ._build_parser (parent , parser_builder )
705
+ if parser is None :
706
+ return
707
+
708
+ # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
709
+ from .decorators import (
710
+ _set_parser_prog ,
711
+ )
712
+
713
+ _set_parser_prog (parser , command )
714
+
715
+ # If the description has not been set, then use the method docstring if one exists
716
+ if parser .description is None and hasattr (command_method , '__wrapped__' ) and command_method .__wrapped__ .__doc__ :
717
+ parser .description = strip_doc_annotations (command_method .__wrapped__ .__doc__ )
718
+
719
+ self ._command_parsers [command ] = parser
720
+
662
721
def _install_command_function (self , command : str , command_wrapper : Callable [..., Any ], context : str = '' ) -> None :
663
722
cmd_func_name = COMMAND_FUNC_PREFIX + command
664
723
@@ -681,6 +740,8 @@ def _install_command_function(self, command: str, command_wrapper: Callable[...,
681
740
self .pwarning (f"Deleting macro '{ command } ' because it shares its name with a new command" )
682
741
del self .macros [command ]
683
742
743
+ self ._register_command_parser (command , command_wrapper )
744
+
684
745
setattr (self , cmd_func_name , command_wrapper )
685
746
686
747
def _install_completer_function (self , cmd_name : str , cmd_completer : CompleterFunc ) -> None :
@@ -727,6 +788,8 @@ def unregister_command_set(self, cmdset: CommandSet) -> None:
727
788
del self ._cmd_to_command_sets [cmd_name ]
728
789
729
790
delattr (self , COMMAND_FUNC_PREFIX + cmd_name )
791
+ if cmd_name in self ._command_parsers :
792
+ del self ._command_parsers [cmd_name ]
730
793
731
794
if hasattr (self , COMPLETER_FUNC_PREFIX + cmd_name ):
732
795
delattr (self , COMPLETER_FUNC_PREFIX + cmd_name )
@@ -746,14 +809,7 @@ def _check_uninstallable(self, cmdset: CommandSet) -> None:
746
809
747
810
for method in methods :
748
811
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 ))
812
+ command_parser = self ._command_parsers .get (command_name , None )
757
813
758
814
def check_parser_uninstallable (parser : argparse .ArgumentParser ) -> None :
759
815
for action in parser ._actions :
@@ -792,7 +848,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
792
848
for method_name , method in methods :
793
849
subcommand_name : str = getattr (method , constants .SUBCMD_ATTR_NAME )
794
850
full_command_name : str = getattr (method , constants .SUBCMD_ATTR_COMMAND )
795
- subcmd_parser = getattr (method , constants .CMD_ATTR_ARGPARSER )
851
+ subcmd_parser_builder = getattr (method , constants .CMD_ATTR_ARGPARSER )
796
852
797
853
subcommand_valid , errmsg = self .statement_parser .is_valid_command (subcommand_name , is_subcommand = True )
798
854
if not subcommand_valid :
@@ -812,7 +868,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
812
868
raise CommandSetRegistrationError (
813
869
f"Could not find command '{ command_name } ' needed by subcommand: { str (method )} "
814
870
)
815
- command_parser = getattr ( command_func , constants . CMD_ATTR_ARGPARSER , None )
871
+ command_parser = self . _command_parsers . get ( command_name , None )
816
872
if command_parser is None :
817
873
raise CommandSetRegistrationError (
818
874
f"Could not find argparser for command '{ command_name } ' needed by subcommand: { str (method )} "
@@ -832,16 +888,17 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) ->
832
888
833
889
target_parser = find_subcommand (command_parser , subcommand_names )
834
890
891
+ subcmd_parser = cast (argparse .ArgumentParser , self ._build_parser (cmdset , subcmd_parser_builder ))
892
+ from .decorators import (
893
+ _set_parser_prog ,
894
+ )
895
+
896
+ _set_parser_prog (subcmd_parser , f'{ command_name } { subcommand_name } ' )
897
+ if subcmd_parser .description is None and method .__doc__ :
898
+ subcmd_parser .description = strip_doc_annotations (method .__doc__ )
899
+
835
900
for action in target_parser ._actions :
836
901
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
-
845
902
# Get the kwargs for add_parser()
846
903
add_parser_kwargs = getattr (method , constants .SUBCMD_ATTR_ADD_PARSER_KWARGS , {})
847
904
@@ -913,7 +970,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
913
970
raise CommandSetRegistrationError (
914
971
f"Could not find command '{ command_name } ' needed by subcommand: { str (method )} "
915
972
)
916
- command_parser = getattr ( command_func , constants . CMD_ATTR_ARGPARSER , None )
973
+ command_parser = self . _command_parsers . get ( command_name , None )
917
974
if command_parser is None : # pragma: no cover
918
975
# This really shouldn't be possible since _register_subcommands would prevent this from happening
919
976
# but keeping in case it does for some strange reason
@@ -2034,7 +2091,7 @@ def _perform_completion(
2034
2091
else :
2035
2092
# There's no completer function, next see if the command uses argparse
2036
2093
func = self .cmd_func (command )
2037
- argparser : Optional [ argparse . ArgumentParser ] = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
2094
+ argparser = self . _command_parsers . get ( command , None )
2038
2095
2039
2096
if func is not None and argparser is not None :
2040
2097
# Get arguments for complete()
@@ -3259,14 +3316,19 @@ def _cmdloop(self) -> None:
3259
3316
#############################################################
3260
3317
3261
3318
# 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
3319
+ @staticmethod
3320
+ def _build_alias_parser () -> argparse .ArgumentParser :
3321
+ alias_description = (
3322
+ "Manage aliases\n " "\n " "An alias is a command that enables replacement of a word by another string."
3323
+ )
3324
+ alias_epilog = "See also:\n " " macro"
3325
+ alias_parser = argparse_custom .DEFAULT_ARGUMENT_PARSER (description = alias_description , epilog = alias_epilog )
3326
+ alias_subparsers = alias_parser .add_subparsers (dest = 'subcommand' , metavar = 'SUBCOMMAND' )
3327
+ alias_subparsers .required = True
3328
+ return alias_parser
3267
3329
3268
3330
# Preserve quotes since we are passing strings to other commands
3269
- @with_argparser (alias_parser , preserve_quotes = True )
3331
+ @with_argparser (_build_alias_parser , preserve_quotes = True )
3270
3332
def do_alias (self , args : argparse .Namespace ) -> None :
3271
3333
"""Manage aliases"""
3272
3334
# Call handler for whatever subcommand was selected
@@ -3681,7 +3743,7 @@ def complete_help_subcommands(
3681
3743
3682
3744
# Check if this command uses argparse
3683
3745
func = self .cmd_func (command )
3684
- argparser = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
3746
+ argparser = self . _command_parsers . get ( command , None )
3685
3747
if func is None or argparser is None :
3686
3748
return []
3687
3749
@@ -3717,7 +3779,7 @@ def do_help(self, args: argparse.Namespace) -> None:
3717
3779
# Getting help for a specific command
3718
3780
func = self .cmd_func (args .command )
3719
3781
help_func = getattr (self , constants .HELP_FUNC_PREFIX + args .command , None )
3720
- argparser = getattr ( func , constants . CMD_ATTR_ARGPARSER , None )
3782
+ argparser = self . _command_parsers . get ( args . command , None )
3721
3783
3722
3784
# If the command function uses argparse, then use argparse's help
3723
3785
if func is not None and argparser is not None :
@@ -3853,7 +3915,7 @@ def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str
3853
3915
help_topics .remove (command )
3854
3916
3855
3917
# Non-argparse commands can have help_functions for their documentation
3856
- if not hasattr ( func , constants . CMD_ATTR_ARGPARSER ) :
3918
+ if command not in self . _command_parsers :
3857
3919
has_help_func = True
3858
3920
3859
3921
if hasattr (func , constants .CMD_ATTR_HELP_CATEGORY ):
@@ -3899,7 +3961,7 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
3899
3961
doc : Optional [str ]
3900
3962
3901
3963
# Non-argparse commands can have help_functions for their documentation
3902
- if not hasattr ( cmd_func , constants . CMD_ATTR_ARGPARSER ) and command in topics :
3964
+ if command not in self . _command_parsers and command in topics :
3903
3965
help_func = getattr (self , constants .HELP_FUNC_PREFIX + command )
3904
3966
result = io .StringIO ()
3905
3967
0 commit comments