From 4c03d88c641da1527066df4cea73c3cd909dffb7 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 Mar 2019 21:17:42 -0500 Subject: [PATCH 01/11] Added way to disable commands --- cmd2/cmd2.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b3a61212d..8a5d31d39 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -279,6 +279,14 @@ class EmptyStatement(Exception): pass +class DisabledCommand: + """Contains data about a disabled command""" + def __init__(self): + # These are used to restore the original functions when the command is enabled + self.command_function = None + self.help_function = None + + class Cmd(cmd.Cmd): """An easy but powerful framework for writing line-oriented command interpreters. @@ -521,6 +529,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent # being printed by a command. self.terminal_lock = threading.RLock() + # Commands that have been disabled from use. This is to support commands that are only available + # during specific states of the application. This dictionary's keys are the command names and its + # values are DisabledCommand objects. + self.disabled_commands = dict() + # ----- Methods related to presenting output to the user ----- @property @@ -1562,7 +1575,7 @@ def get_all_commands(self) -> List[str]: if name.startswith(COMMAND_FUNC_PREFIX) and callable(getattr(self, name))] def get_visible_commands(self) -> List[str]: - """Returns a list of commands that have not been hidden.""" + """Returns a list of commands that have not been hidden or disabled.""" commands = self.get_all_commands() # Remove the hidden commands @@ -1570,6 +1583,11 @@ def get_visible_commands(self) -> List[str]: if name in commands: commands.remove(name) + # Remove the disabled commands + for name in self.disabled_commands: + if name in commands: + commands.remove(name) + return commands def get_alias_names(self) -> List[str]: @@ -1953,7 +1971,7 @@ def cmd_func_name(self, command: str) -> str: def onecmd(self, statement: Union[Statement, str]) -> bool: """ This executes the actual do_* method for a command. - If the command provided doesn't exist, then it executes _default() instead. + If the command provided doesn't exist, then it executes default() instead. :param statement: intended to be a Statement instance parsed command from the input stream, alternative acceptance of a str is present only for backward compatibility with cmd @@ -3186,13 +3204,15 @@ def do_history(self, args: argparse.Namespace) -> None: # -v must be used alone with no other options if args.verbose: - if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script: + if (args.clear or args.edit or args.output_file or args.run or + args.transcript or args.expanded or args.script): self.poutput("-v can not be used with any other options") self.poutput(self.history_parser.format_usage()) return # -s and -x can only be used if none of these options are present: [-c -r -e -o -t] - if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript): + if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or + args.transcript): self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t") self.poutput(self.history_parser.format_usage()) return @@ -3598,6 +3618,85 @@ def set_window_title(self, title: str) -> None: # pragma: no cover else: raise RuntimeError("another thread holds terminal_lock") + def enable_command(self, command: str) -> None: + """ + Enable a command by restoring its functions + :param command: the command being enabled + """ + # If the commands is already enabled, then return + if command not in self.disabled_commands: + return + + help_func_name = HELP_FUNC_PREFIX + command + + # Restore the command and help functions to their original values + dc = self.disabled_commands[command] + setattr(self, self.cmd_func_name(command), dc.command_function) + + if dc.help_function is None: + delattr(self, help_func_name) + else: + setattr(self, help_func_name, dc.help_function) + + # Remove the disabled command entry + del self.disabled_commands[command] + + def enable_category(self, category: str) -> None: + """ + Enable an entire category of commands + :param category: the category to enable + """ + for cmd_name in self.disabled_commands: + dc = self.disabled_commands[cmd_name] + cmd_category = getattr(dc.command_function, HELP_CATEGORY, None) + if cmd_category is not None and cmd_category == category: + self.enable_command(cmd_name) + + def disable_command(self, command: str, message_to_print: str) -> None: + """ + Disable a command and overwrite its functions + :param command: the command being disabled + :param message_to_print: what to print when this command or its help function is run while disabled + """ + import functools + + # If the commands is already disabled, then return + if command in self.disabled_commands: + return + + # Make sure this is an actual command + command_function = self.cmd_func(command) + if command_function is None: + raise AttributeError("{} does not refer to a command".format(command)) + + help_func_name = HELP_FUNC_PREFIX + command + + # Add the disabled command record + dc = DisabledCommand() + dc.command_function = command_function + dc.help_function = getattr(self, help_func_name, None) + self.disabled_commands[command] = dc + + # Overwrite the command and help functions to print the message + setattr(self, self.cmd_func_name(command), functools.partial(self.poutput, message_to_print + '\n')) + setattr(self, help_func_name, functools.partial(self.poutput, message_to_print + '\n')) + + def disable_category(self, category: str, message_to_print: str) -> None: + """ + Disable an entire category of commands + :param category: the category to disable + :param message_to_print: what to print when anything in this category is run while disabled + """ + all_commands = self.get_all_commands() + + for cmd_name in all_commands: + func = self.cmd_func(cmd_name) + cmd_category = getattr(func, HELP_CATEGORY, None) + + # If this command is in the category, then disable it + if cmd_category is not None and cmd_category == category: + self.disable_command(cmd_name, message_to_print) + def cmdloop(self, intro: Optional[str] = None) -> None: """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. From 0b84d1b5ecb1d846aecb488e9293009f0d543183 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 Mar 2019 23:52:24 -0500 Subject: [PATCH 02/11] Added unit tests for disabling commands --- cmd2/cmd2.py | 2 +- tests/test_cmd2.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e50a192d1..f3449a8b4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3646,7 +3646,7 @@ def enable_category(self, category: str) -> None: Enable an entire category of commands :param category: the category to enable """ - for cmd_name in self.disabled_commands: + for cmd_name in list(self.disabled_commands): dc = self.disabled_commands[cmd_name] cmd_category = getattr(dc.command_function, HELP_CATEGORY, None) if cmd_category is not None and cmd_category == category: diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index d43f77867..45051f3ae 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -2170,3 +2170,92 @@ def test_colors_never_notty(mocker, capsys): app.onecmd_plus_hooks('echo oopsie') out, err = capsys.readouterr() assert out == err == 'oopsie\n' + + +class DisableCommandsApp(cmd2.Cmd): + """Class for disabling commands""" + category_name = "Test Category" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @cmd2.with_category(category_name) + def do_has_help_func(self, arg): + self.poutput("The real has_help_func") + + def help_has_help_func(self): + self.poutput('Help for has_help_func') + + @cmd2.with_category(category_name) + def do_has_no_help_func(self, arg): + """Help for has_no_help_func""" + self.poutput("The real has_no_help_func") + + +@pytest.fixture +def disable_commands_app(): + app = DisableCommandsApp() + app.stdout = utils.StdSim(app.stdout) + return app + + +def test_disable_and_enable_category(disable_commands_app): + # Disable the category + message_to_print = 'These commands are currently disabled' + disable_commands_app.disable_category(disable_commands_app.category_name, message_to_print) + + # Make sure all the commands and help on those commands displays the message + out = run_cmd(disable_commands_app, 'has_help_func') + assert out == [message_to_print] + + out = run_cmd(disable_commands_app, 'help has_help_func') + assert out == [message_to_print] + + out = run_cmd(disable_commands_app, 'has_no_help_func') + assert out == [message_to_print] + + out = run_cmd(disable_commands_app, 'help has_no_help_func') + assert out == [message_to_print] + + # Enable the category + disable_commands_app.enable_category(disable_commands_app.category_name) + + # Make sure all the commands and help on those commands are restored + out = run_cmd(disable_commands_app, 'has_help_func') + assert out == ["The real has_help_func"] + + out = run_cmd(disable_commands_app, 'help has_help_func') + assert out == ["Help for has_help_func"] + + out = run_cmd(disable_commands_app, 'has_no_help_func') + assert out == ["The real has_no_help_func"] + + out = run_cmd(disable_commands_app, 'help has_no_help_func') + assert out == ["Help for has_no_help_func"] + +def test_enable_enabled_command(disable_commands_app): + # Test enabling a command that is not disabled + saved_len = len(disable_commands_app.disabled_commands) + disable_commands_app.enable_command('has_help_func') + + # The number of disabled_commands should not have changed + assert saved_len == len(disable_commands_app.disabled_commands) + +def test_disable_fake_command(disable_commands_app): + with pytest.raises(AttributeError): + disable_commands_app.disable_command('fake', 'fake message') + +def test_disable_command_twice(disable_commands_app): + saved_len = len(disable_commands_app.disabled_commands) + message_to_print = 'These commands are currently disabled' + disable_commands_app.disable_command('has_help_func', message_to_print) + + # The length of disabled_commands should have increased one + new_len = len(disable_commands_app.disabled_commands) + assert saved_len == new_len - 1 + saved_len = new_len + + # Disable again and the length should not change + disable_commands_app.disable_command('has_help_func', message_to_print) + new_len = len(disable_commands_app.disabled_commands) + assert saved_len == new_len From f797daef64a529d205cf9db9a9b47e889686fa98 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 9 Mar 2019 23:56:34 -0500 Subject: [PATCH 03/11] Fixed flake warnings --- cmd2/cmd2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f3449a8b4..479bec08e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3204,15 +3204,15 @@ def do_history(self, args: argparse.Namespace) -> None: # -v must be used alone with no other options if args.verbose: - if (args.clear or args.edit or args.output_file or args.run or - args.transcript or args.expanded or args.script): + if (args.clear or args.edit or args.output_file or args.run + or args.transcript or args.expanded or args.script): self.poutput("-v can not be used with any other options") self.poutput(self.history_parser.format_usage()) return # -s and -x can only be used if none of these options are present: [-c -r -e -o -t] - if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or - args.transcript): + if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run + or args.transcript): self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t") self.poutput(self.history_parser.format_usage()) return From 67cea8267051134179014bc167b79e0b0b5132d9 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 00:02:10 -0500 Subject: [PATCH 04/11] Increased code coverage --- tests/test_cmd2.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 45051f3ae..5e9e4ce63 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -2217,6 +2217,10 @@ def test_disable_and_enable_category(disable_commands_app): out = run_cmd(disable_commands_app, 'help has_no_help_func') assert out == [message_to_print] + visible_commands = disable_commands_app.get_visible_commands() + assert 'has_help_func' not in visible_commands + assert 'has_no_help_func' not in visible_commands + # Enable the category disable_commands_app.enable_category(disable_commands_app.category_name) @@ -2233,6 +2237,10 @@ def test_disable_and_enable_category(disable_commands_app): out = run_cmd(disable_commands_app, 'help has_no_help_func') assert out == ["Help for has_no_help_func"] + visible_commands = disable_commands_app.get_visible_commands() + assert 'has_help_func' in visible_commands + assert 'has_no_help_func' in visible_commands + def test_enable_enabled_command(disable_commands_app): # Test enabling a command that is not disabled saved_len = len(disable_commands_app.disabled_commands) From 39751265531389bc864d536f992baf80a2038211 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 00:14:43 -0500 Subject: [PATCH 05/11] Fixed another Flake warning --- cmd2/cmd2.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 479bec08e..c437c9d5a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3204,15 +3204,13 @@ def do_history(self, args: argparse.Namespace) -> None: # -v must be used alone with no other options if args.verbose: - if (args.clear or args.edit or args.output_file or args.run - or args.transcript or args.expanded or args.script): + if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script: self.poutput("-v can not be used with any other options") self.poutput(self.history_parser.format_usage()) return # -s and -x can only be used if none of these options are present: [-c -r -e -o -t] - if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run - or args.transcript): + if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript): self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t") self.poutput(self.history_parser.format_usage()) return From 102b6c121ccaa645fff02be0928a7342746232b8 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 00:33:42 -0500 Subject: [PATCH 06/11] Fixed typo in example --- examples/help_categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/help_categories.py b/examples/help_categories.py index 50b2c17d2..f638e0dfb 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -39,7 +39,7 @@ def do_list(self, _): def do_deploy(self, _): """Deploy command""" - self.poutput('Which') + self.poutput('Deploy') def do_start(self, _): """Start command""" From 647f0727cf8e1c345d7b1e778e1132479f2168fb Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 01:39:36 -0500 Subject: [PATCH 07/11] Updated examples and documentation pertaining to disabling commands --- CHANGELOG.md | 8 ++ cmd2/cmd2.py | 5 +- docs/argument_processing.rst | 132 ---------------------------- docs/unfreefeatures.rst | 162 +++++++++++++++++++++++++++++++++++ examples/help_categories.py | 12 +++ 5 files changed, 185 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf90db8c..68c56d3c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ * Added **-v**, **--verbose** flag * display history and include expanded commands if they differ from the typed command * Added ``matches_sort_key`` to override the default way tab completion matches are sorted + * Added ability to disable/enable individual commands and entire categories of commands. When a command + is disabled, it will not show up in the help menu or tab complete. If a user tries to run the command + or call help on it, a command-specific message supplied by the developer will be printed. The following + commands were added to support this feature. + * ``enable_command()`` + * ``enable_category()`` + * ``disable_command()`` + * ``disable_category()`` * Potentially breaking changes * Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now requires that it can't be ``None``. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c437c9d5a..c9661a1a3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3654,7 +3654,7 @@ def disable_command(self, command: str, message_to_print: str) -> None: """ Disable a command and overwrite its functions :param command: the command being disabled - :param message_to_print: what to print when this command or its help function is run while disabled + :param message_to_print: what to print when this command is run or help is called on it while disabled """ import functools @@ -3683,7 +3683,8 @@ def disable_category(self, category: str, message_to_print: str) -> None: """ Disable an entire category of commands :param category: the category to disable - :param message_to_print: what to print when anything in this category is run while disabled + :param message_to_print: what to print when anything in this category is run or help is called on it + while disabled """ all_commands = self.get_all_commands() diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst index 4c77fa803..bad683bf9 100644 --- a/docs/argument_processing.rst +++ b/docs/argument_processing.rst @@ -195,138 +195,6 @@ Which yields: .. _argparse: https://docs.python.org/3/library/argparse.html -Grouping Commands -================= - -By default, the ``help`` command displays:: - - Documented commands (type help ): - ======================================== - alias findleakers pyscript sessions status vminfo - config help quit set stop which - connect history redeploy shell thread_dump - deploy list resources shortcuts unalias - edit load restart sslconnectorciphers undeploy - expire py serverinfo start version - -If you have a large number of commands, you can optionally group your commands into categories. -Here's the output from the example ``help_categories.py``:: - - Documented commands (type help ): - - Application Management - ====================== - deploy findleakers redeploy sessions stop - expire list restart start undeploy - - Connecting - ========== - connect which - - Server Information - ================== - resources serverinfo sslconnectorciphers status thread_dump vminfo - - Other - ===== - alias edit history py quit shell unalias - config help load pyscript set shortcuts version - - -There are 2 methods of specifying command categories, using the ``@with_category`` decorator or with the -``categorize()`` function. Once a single command category is detected, the help output switches to a categorized -mode of display. All commands with an explicit category defined default to the category `Other`. - -Using the ``@with_category`` decorator:: - - @with_category(CMD_CAT_CONNECTING) - def do_which(self, _): - """Which command""" - self.poutput('Which') - -Using the ``categorize()`` function: - - You can call with a single function:: - - def do_connect(self, _): - """Connect command""" - self.poutput('Connect') - - # Tag the above command functions under the category Connecting - categorize(do_connect, CMD_CAT_CONNECTING) - - Or with an Iterable container of functions:: - - def do_undeploy(self, _): - """Undeploy command""" - self.poutput('Undeploy') - - def do_stop(self, _): - """Stop command""" - self.poutput('Stop') - - def do_findleakers(self, _): - """Find Leakers command""" - self.poutput('Find Leakers') - - # Tag the above command functions under the category Application Management - categorize((do_undeploy, - do_stop, - do_findleakers), CMD_CAT_APP_MGMT) - -The ``help`` command also has a verbose option (``help -v`` or ``help --verbose``) that combines -the help categories with per-command Help Messages:: - - Documented commands (type help ): - - Application Management - ================================================================================ - deploy Deploy command - expire Expire command - findleakers Find Leakers command - list List command - redeploy Redeploy command - restart usage: restart [-h] {now,later,sometime,whenever} - sessions Sessions command - start Start command - stop Stop command - undeploy Undeploy command - - Connecting - ================================================================================ - connect Connect command - which Which command - - Server Information - ================================================================================ - resources Resources command - serverinfo Server Info command - sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains - multiple lines of help information for the user. Each line of help in a - contiguous set of lines will be printed and aligned in the verbose output - provided with 'help --verbose' - status Status command - thread_dump Thread Dump command - vminfo VM Info command - - Other - ================================================================================ - alias Define or display aliases - config Config command - edit Edit a file in a text editor - help List available commands with "help" or detailed help with "help cmd" - history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg] - load Runs commands in script file that is encoded as either ASCII or UTF-8 text - py Invoke python command, shell, or script - pyscript Runs a python script file inside the console - quit Exits this application - set usage: set [-h] [-a] [-l] [settable [settable ...]] - shell Execute a command as if at the OS prompt - shortcuts Lists shortcuts available - unalias Unsets aliases - version Version command - - Receiving an argument list ========================== diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst index 2cdc17ba3..979532152 100644 --- a/docs/unfreefeatures.rst +++ b/docs/unfreefeatures.rst @@ -237,3 +237,165 @@ set_window_title() The easiest way to understand these functions is to see the AsyncPrinting_ example for a demonstration. .. _AsyncPrinting: https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py + + +Grouping Commands +================= + +By default, the ``help`` command displays:: + + Documented commands (type help ): + ======================================== + alias findleakers pyscript sessions status vminfo + config help quit set stop which + connect history redeploy shell thread_dump + deploy list resources shortcuts unalias + edit load restart sslconnectorciphers undeploy + expire py serverinfo start version + +If you have a large number of commands, you can optionally group your commands into categories. +Here's the output from the example ``help_categories.py``:: + + Documented commands (type help ): + + Application Management + ====================== + deploy findleakers redeploy sessions stop + expire list restart start undeploy + + Connecting + ========== + connect which + + Server Information + ================== + resources serverinfo sslconnectorciphers status thread_dump vminfo + + Other + ===== + alias edit history py quit shell unalias + config help load pyscript set shortcuts version + + +There are 2 methods of specifying command categories, using the ``@with_category`` decorator or with the +``categorize()`` function. Once a single command category is detected, the help output switches to a categorized +mode of display. All commands with an explicit category defined default to the category `Other`. + +Using the ``@with_category`` decorator:: + + @with_category(CMD_CAT_CONNECTING) + def do_which(self, _): + """Which command""" + self.poutput('Which') + +Using the ``categorize()`` function: + + You can call with a single function:: + + def do_connect(self, _): + """Connect command""" + self.poutput('Connect') + + # Tag the above command functions under the category Connecting + categorize(do_connect, CMD_CAT_CONNECTING) + + Or with an Iterable container of functions:: + + def do_undeploy(self, _): + """Undeploy command""" + self.poutput('Undeploy') + + def do_stop(self, _): + """Stop command""" + self.poutput('Stop') + + def do_findleakers(self, _): + """Find Leakers command""" + self.poutput('Find Leakers') + + # Tag the above command functions under the category Application Management + categorize((do_undeploy, + do_stop, + do_findleakers), CMD_CAT_APP_MGMT) + +The ``help`` command also has a verbose option (``help -v`` or ``help --verbose``) that combines +the help categories with per-command Help Messages:: + + Documented commands (type help ): + + Application Management + ================================================================================ + deploy Deploy command + expire Expire command + findleakers Find Leakers command + list List command + redeploy Redeploy command + restart usage: restart [-h] {now,later,sometime,whenever} + sessions Sessions command + start Start command + stop Stop command + undeploy Undeploy command + + Connecting + ================================================================================ + connect Connect command + which Which command + + Server Information + ================================================================================ + resources Resources command + serverinfo Server Info command + sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains + multiple lines of help information for the user. Each line of help in a + contiguous set of lines will be printed and aligned in the verbose output + provided with 'help --verbose' + status Status command + thread_dump Thread Dump command + vminfo VM Info command + + Other + ================================================================================ + alias Define or display aliases + config Config command + edit Edit a file in a text editor + help List available commands with "help" or detailed help with "help cmd" + history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg] + load Runs commands in script file that is encoded as either ASCII or UTF-8 text + py Invoke python command, shell, or script + pyscript Runs a python script file inside the console + quit Exits this application + set usage: set [-h] [-a] [-l] [settable [settable ...]] + shell Execute a command as if at the OS prompt + shortcuts Lists shortcuts available + unalias Unsets aliases + version Version command + + +Disabling Commands +================== + +``cmd2`` supports disabling commands during runtime. This is useful if certain commands should only be available +when the application is in a specific state. When a command is disabled, it will not show up in the help menu or +tab complete. If a user tries to run the command, a command-specific message supplied by the developer will be +printed. The following functions support this feature. + +enable_command() + Enable an individual command + +enable_category() + Enable an entire category of commands + +disable_command() + Disable an individual command and set the message that will print when this command is run or help is called + on it while disabled + +disable_category() + Disable an entire category of commands and set the message that will print when anything in this category is + run or help is called on it while disabled + +See the definitions of these functions for descriptions of their arguments. + +See the ``do_enable_commands()`` and ``do_disable_commands()`` functions in the HelpCategories_ example for +a demonstration. + +.. _HelpCategories: https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py diff --git a/examples/help_categories.py b/examples/help_categories.py index f638e0dfb..e58bac02c 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -140,6 +140,18 @@ def do_version(self, _): """Version command""" self.poutput(cmd2.__version__) + @cmd2.with_category("Command Management") + def do_disable_commands(self, _): + """Disable the Application Management commands""" + self.disable_category(self.CMD_CAT_APP_MGMT, "Application Management is currently disabled") + self.poutput("The Application Management commands have been disabled") + + @cmd2.with_category("Command Management") + def do_enable_commands(self, _): + """Enable the Application Management commands""" + self.enable_category(self.CMD_CAT_APP_MGMT) + self.poutput("The Application Management commands have been enabled") + if __name__ == '__main__': c = HelpCategories() From a1afa7cc454c476280dda3201e8ed21d4fce9dfd Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 13:44:39 -0400 Subject: [PATCH 08/11] Added function to report that a disabled command has been run. Not adding disabled commands to the history when run. --- cmd2/cmd2.py | 20 ++++++++++++++++---- tests/test_cmd2.py | 8 ++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c9661a1a3..957ebffd2 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1987,8 +1987,9 @@ def onecmd(self, statement: Union[Statement, str]) -> bool: else: func = self.cmd_func(statement.command) if func: - # Since we have a valid command store it in the history - if statement.command not in self.exclude_from_history: + # Check to see if this command should be stored in history + if statement.command not in self.exclude_from_history \ + and statement.command not in self.disabled_commands: self.history.append(statement) stop = func(statement) @@ -3676,8 +3677,9 @@ def disable_command(self, command: str, message_to_print: str) -> None: self.disabled_commands[command] = dc # Overwrite the command and help functions to print the message - setattr(self, self.cmd_func_name(command), functools.partial(self.poutput, message_to_print + '\n')) - setattr(self, help_func_name, functools.partial(self.poutput, message_to_print + '\n')) + new_func = functools.partial(self._report_disabled_command_usage, message_to_print=message_to_print) + setattr(self, self.cmd_func_name(command), new_func) + setattr(self, help_func_name, new_func) def disable_category(self, category: str, message_to_print: str) -> None: """ @@ -3696,6 +3698,16 @@ def disable_category(self, category: str, message_to_print: str) -> None: if cmd_category is not None and cmd_category == category: self.disable_command(cmd_name, message_to_print) + # noinspection PyUnusedLocal + def _report_disabled_command_usage(self, *args, message_to_print: str, **kwargs) -> None: + """ + Report when a disabled command has been run or had help called on it + :param args: not used + :param message_to_print: the message reporting that the command is disabled + :param kwargs: not used + """ + self.poutput(message_to_print) + def cmdloop(self, intro: Optional[str] = None) -> None: """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 5e9e4ce63..b3942203d 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -2267,3 +2267,11 @@ def test_disable_command_twice(disable_commands_app): disable_commands_app.disable_command('has_help_func', message_to_print) new_len = len(disable_commands_app.disabled_commands) assert saved_len == new_len + +def test_disabled_command_not_in_history(disable_commands_app): + message_to_print = 'These commands are currently disabled' + disable_commands_app.disable_command('has_help_func', message_to_print) + + saved_len = len(disable_commands_app.history) + run_cmd(disable_commands_app, 'has_help_func') + assert saved_len == len(disable_commands_app.history) From 00df9e4e3b124fc771daab69a516d5bbfb828af8 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 14:12:29 -0400 Subject: [PATCH 09/11] Fixed long line warning --- cmd2/cmd2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 957ebffd2..f0fa51afe 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3211,7 +3211,8 @@ def do_history(self, args: argparse.Namespace) -> None: return # -s and -x can only be used if none of these options are present: [-c -r -e -o -t] - if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript): + if (args.script or args.expanded) \ + and (args.clear or args.edit or args.output_file or args.run or args.transcript): self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t") self.poutput(self.history_parser.format_usage()) return From 69ab1e12e5fce85ad6dfbb0dcf1389f2b7bbeabe Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 14:17:16 -0400 Subject: [PATCH 10/11] Fixed long line warning --- cmd2/cmd2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f0fa51afe..9d1a79d1e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3205,7 +3205,8 @@ def do_history(self, args: argparse.Namespace) -> None: # -v must be used alone with no other options if args.verbose: - if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script: + if args.clear or args.edit or args.output_file or args.run or args.transcript \ + or args.expanded or args.script: self.poutput("-v can not be used with any other options") self.poutput(self.history_parser.format_usage()) return From 1a395130b720671c2e0be26f4f04c8adb98e5703 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 10 Mar 2019 14:38:58 -0400 Subject: [PATCH 11/11] Changed DisabledCommand into a namedtuple --- cmd2/cmd2.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9d1a79d1e..1767cc677 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -37,6 +37,7 @@ import re import sys import threading +from collections import namedtuple from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO import colorama @@ -279,12 +280,8 @@ class EmptyStatement(Exception): pass -class DisabledCommand: - """Contains data about a disabled command""" - def __init__(self): - # These are used to restore the original functions when the command is enabled - self.command_function = None - self.help_function = None +# Contains data about a disabled command which is used to restore its original functions when the command is enabled +DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function']) class Cmd(cmd.Cmd): @@ -3673,10 +3670,8 @@ def disable_command(self, command: str, message_to_print: str) -> None: help_func_name = HELP_FUNC_PREFIX + command # Add the disabled command record - dc = DisabledCommand() - dc.command_function = command_function - dc.help_function = getattr(self, help_func_name, None) - self.disabled_commands[command] = dc + self.disabled_commands[command] = DisabledCommand(command_function=command_function, + help_function=getattr(self, help_func_name, None)) # Overwrite the command and help functions to print the message new_func = functools.partial(self._report_disabled_command_usage, message_to_print=message_to_print)