3737import re
3838import sys
3939import threading
40+ from collections import namedtuple
4041from typing import Any , Callable , Dict , List , Mapping , Optional , Tuple , Type , Union , IO
4142
4243import colorama
@@ -279,6 +280,10 @@ class EmptyStatement(Exception):
279280 pass
280281
281282
283+ # Contains data about a disabled command which is used to restore its original functions when the command is enabled
284+ DisabledCommand = namedtuple ('DisabledCommand' , ['command_function' , 'help_function' ])
285+
286+
282287class Cmd (cmd .Cmd ):
283288 """An easy but powerful framework for writing line-oriented command interpreters.
284289
@@ -521,6 +526,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
521526 # being printed by a command.
522527 self .terminal_lock = threading .RLock ()
523528
529+ # Commands that have been disabled from use. This is to support commands that are only available
530+ # during specific states of the application. This dictionary's keys are the command names and its
531+ # values are DisabledCommand objects.
532+ self .disabled_commands = dict ()
533+
524534 # ----- Methods related to presenting output to the user -----
525535
526536 @property
@@ -587,7 +597,7 @@ def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_col
587597 :param err_color: (optional) color escape to output error with
588598 :param war_color: (optional) color escape to output warning with
589599 """
590- if self .debug :
600+ if self .debug and sys . exc_info () != ( None , None , None ) :
591601 import traceback
592602 traceback .print_exc ()
593603
@@ -1562,14 +1572,19 @@ def get_all_commands(self) -> List[str]:
15621572 if name .startswith (COMMAND_FUNC_PREFIX ) and callable (getattr (self , name ))]
15631573
15641574 def get_visible_commands (self ) -> List [str ]:
1565- """Returns a list of commands that have not been hidden."""
1575+ """Returns a list of commands that have not been hidden or disabled ."""
15661576 commands = self .get_all_commands ()
15671577
15681578 # Remove the hidden commands
15691579 for name in self .hidden_commands :
15701580 if name in commands :
15711581 commands .remove (name )
15721582
1583+ # Remove the disabled commands
1584+ for name in self .disabled_commands :
1585+ if name in commands :
1586+ commands .remove (name )
1587+
15731588 return commands
15741589
15751590 def get_alias_names (self ) -> List [str ]:
@@ -1953,7 +1968,7 @@ def cmd_func_name(self, command: str) -> str:
19531968 def onecmd (self , statement : Union [Statement , str ]) -> bool :
19541969 """ This executes the actual do_* method for a command.
19551970
1956- If the command provided doesn't exist, then it executes _default () instead.
1971+ If the command provided doesn't exist, then it executes default () instead.
19571972
19581973 :param statement: intended to be a Statement instance parsed command from the input stream, alternative
19591974 acceptance of a str is present only for backward compatibility with cmd
@@ -1969,8 +1984,9 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
19691984 else :
19701985 func = self .cmd_func (statement .command )
19711986 if func :
1972- # Since we have a valid command store it in the history
1973- if statement .command not in self .exclude_from_history :
1987+ # Check to see if this command should be stored in history
1988+ if statement .command not in self .exclude_from_history \
1989+ and statement .command not in self .disabled_commands :
19741990 self .history .append (statement )
19751991
19761992 stop = func (statement )
@@ -3186,13 +3202,15 @@ def do_history(self, args: argparse.Namespace) -> None:
31863202
31873203 # -v must be used alone with no other options
31883204 if args .verbose :
3189- if args .clear or args .edit or args .output_file or args .run or args .transcript or args .expanded or args .script :
3205+ if args .clear or args .edit or args .output_file or args .run or args .transcript \
3206+ or args .expanded or args .script :
31903207 self .poutput ("-v can not be used with any other options" )
31913208 self .poutput (self .history_parser .format_usage ())
31923209 return
31933210
31943211 # -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
3195- if (args .script or args .expanded ) and (args .clear or args .edit or args .output_file or args .run or args .transcript ):
3212+ if (args .script or args .expanded ) \
3213+ and (args .clear or args .edit or args .output_file or args .run or args .transcript ):
31963214 self .poutput ("-s and -x can not be used with -c, -r, -e, -o, or -t" )
31973215 self .poutput (self .history_parser .format_usage ())
31983216 return
@@ -3605,6 +3623,95 @@ def set_window_title(self, title: str) -> None: # pragma: no cover
36053623 else :
36063624 raise RuntimeError ("another thread holds terminal_lock" )
36073625
3626+ def enable_command (self , command : str ) -> None :
3627+ """
3628+ Enable a command by restoring its functions
3629+ :param command: the command being enabled
3630+ """
3631+ # If the commands is already enabled, then return
3632+ if command not in self .disabled_commands :
3633+ return
3634+
3635+ help_func_name = HELP_FUNC_PREFIX + command
3636+
3637+ # Restore the command and help functions to their original values
3638+ dc = self .disabled_commands [command ]
3639+ setattr (self , self .cmd_func_name (command ), dc .command_function )
3640+
3641+ if dc .help_function is None :
3642+ delattr (self , help_func_name )
3643+ else :
3644+ setattr (self , help_func_name , dc .help_function )
3645+
3646+ # Remove the disabled command entry
3647+ del self .disabled_commands [command ]
3648+
3649+ def enable_category (self , category : str ) -> None :
3650+ """
3651+ Enable an entire category of commands
3652+ :param category: the category to enable
3653+ """
3654+ for cmd_name in list (self .disabled_commands ):
3655+ dc = self .disabled_commands [cmd_name ]
3656+ cmd_category = getattr (dc .command_function , HELP_CATEGORY , None )
3657+ if cmd_category is not None and cmd_category == category :
3658+ self .enable_command (cmd_name )
3659+
3660+ def disable_command (self , command : str , message_to_print : str ) -> None :
3661+ """
3662+ Disable a command and overwrite its functions
3663+ :param command: the command being disabled
3664+ :param message_to_print: what to print when this command is run or help is called on it while disabled
3665+ """
3666+ import functools
3667+
3668+ # If the commands is already disabled, then return
3669+ if command in self .disabled_commands :
3670+ return
3671+
3672+ # Make sure this is an actual command
3673+ command_function = self .cmd_func (command )
3674+ if command_function is None :
3675+ raise AttributeError ("{} does not refer to a command" .format (command ))
3676+
3677+ help_func_name = HELP_FUNC_PREFIX + command
3678+
3679+ # Add the disabled command record
3680+ self .disabled_commands [command ] = DisabledCommand (command_function = command_function ,
3681+ help_function = getattr (self , help_func_name , None ))
3682+
3683+ # Overwrite the command and help functions to print the message
3684+ new_func = functools .partial (self ._report_disabled_command_usage , message_to_print = message_to_print )
3685+ setattr (self , self .cmd_func_name (command ), new_func )
3686+ setattr (self , help_func_name , new_func )
3687+
3688+ def disable_category (self , category : str , message_to_print : str ) -> None :
3689+ """
3690+ Disable an entire category of commands
3691+ :param category: the category to disable
3692+ :param message_to_print: what to print when anything in this category is run or help is called on it
3693+ while disabled
3694+ """
3695+ all_commands = self .get_all_commands ()
3696+
3697+ for cmd_name in all_commands :
3698+ func = self .cmd_func (cmd_name )
3699+ cmd_category = getattr (func , HELP_CATEGORY , None )
3700+
3701+ # If this command is in the category, then disable it
3702+ if cmd_category is not None and cmd_category == category :
3703+ self .disable_command (cmd_name , message_to_print )
3704+
3705+ # noinspection PyUnusedLocal
3706+ def _report_disabled_command_usage (self , * args , message_to_print : str , ** kwargs ) -> None :
3707+ """
3708+ Report when a disabled command has been run or had help called on it
3709+ :param args: not used
3710+ :param message_to_print: the message reporting that the command is disabled
3711+ :param kwargs: not used
3712+ """
3713+ self .poutput (message_to_print )
3714+
36083715 def cmdloop (self , intro : Optional [str ] = None ) -> None :
36093716 """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
36103717
0 commit comments