summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-03-10 15:02:24 -0400
committerGitHub <noreply@github.com>2019-03-10 15:02:24 -0400
commit290f2246132320c1ab94c427cc662b0fe62c6058 (patch)
treecb4df938785c40a35ed0aac28a50d7df73678d5a
parentd9cd632651d01f87bd599feb75653cd0dde9497e (diff)
parentf787f474d47dc871db337c07e31c3e7d92d4b6f8 (diff)
downloadcmd2-git-290f2246132320c1ab94c427cc662b0fe62c6058.tar.gz
Merge pull request #643 from python-cmd2/disable_command
Disable commands
-rw-r--r--CHANGELOG.md8
-rw-r--r--cmd2/cmd2.py119
-rw-r--r--docs/argument_processing.rst132
-rw-r--r--docs/unfreefeatures.rst162
-rwxr-xr-xexamples/help_categories.py14
-rw-r--r--tests/test_cmd2.py105
6 files changed, 401 insertions, 139 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7a985b7..043962ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,14 @@
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
* Added ``StdSim.pause_storage`` member which when True will cause ``StdSim`` to not save the output sent to it.
See documentation for ``CommandResult`` in ``pyscript_bridge.py`` for reasons pausing the storage can be useful.
+ * 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 0d1d0b51..1767cc67 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -37,6 +37,7 @@ import os
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,6 +280,10 @@ class EmptyStatement(Exception):
pass
+# 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):
"""An easy but powerful framework for writing line-oriented command interpreters.
@@ -521,6 +526,11 @@ class Cmd(cmd.Cmd):
# 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 +1572,7 @@ class Cmd(cmd.Cmd):
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 +1580,11 @@ class Cmd(cmd.Cmd):
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 +1968,7 @@ class Cmd(cmd.Cmd):
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
@@ -1969,8 +1984,9 @@ class Cmd(cmd.Cmd):
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)
@@ -3186,13 +3202,15 @@ class Cmd(cmd.Cmd):
# -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 +3616,95 @@ class Cmd(cmd.Cmd):
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 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:
+ 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 is run or help is called on it 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
+ 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)
+ 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:
+ """
+ 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 or help is called on it
+ 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)
+
+ # 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/docs/argument_processing.rst b/docs/argument_processing.rst
index 4c77fa80..bad683bf 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 <topic>):
- ========================================
- 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 <topic>):
-
- 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 <topic>):
-
- 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 2cdc17ba..97953215 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 <topic>):
+ ========================================
+ 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 <topic>):
+
+ 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 <topic>):
+
+ 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 50b2c17d..e58bac02 100755
--- a/examples/help_categories.py
+++ b/examples/help_categories.py
@@ -39,7 +39,7 @@ class HelpCategories(cmd2.Cmd):
def do_deploy(self, _):
"""Deploy command"""
- self.poutput('Which')
+ self.poutput('Deploy')
def do_start(self, _):
"""Start command"""
@@ -140,6 +140,18 @@ class HelpCategories(cmd2.Cmd):
"""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()
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index d43f7786..b3942203 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -2170,3 +2170,108 @@ 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]
+
+ 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)
+
+ # 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"]
+
+ 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)
+ 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
+
+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)