summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2019-03-10 15:50:20 -0600
committerkotfu <kotfu@kotfu.net>2019-03-10 15:50:20 -0600
commitf67166753e52f308cd33794d31f5e072c14e2f55 (patch)
tree02c992bd9cca8ccbf719d2effbb7c696051a2826
parent1c302bc191b8370a3e85399ec65a20670dc83b8f (diff)
parent290f2246132320c1ab94c427cc662b0fe62c6058 (diff)
downloadcmd2-git-f67166753e52f308cd33794d31f5e072c14e2f55.tar.gz
Merge branch 'history_improvements' of github.com:python-cmd2/cmd2 into history_improvements
-rw-r--r--CHANGELOG.md11
-rw-r--r--cmd2/cmd2.py121
-rw-r--r--cmd2/pyscript_bridge.py38
-rw-r--r--cmd2/utils.py47
-rw-r--r--docs/argument_processing.rst132
-rw-r--r--docs/freefeatures.rst20
-rw-r--r--docs/unfreefeatures.rst162
-rwxr-xr-xexamples/help_categories.py14
-rw-r--r--tests/test_cmd2.py105
-rw-r--r--tests/test_utils.py22
10 files changed, 482 insertions, 190 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcf90db8..043962ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
* Bug Fixes
* Fixed bug in how **history** command deals with multiline commands when output to a script
* Fixed a bug when the ``with_argument_list`` decorator is called with the optional ``preserve_quotes`` argument
+ * Fix bug in ``perror()`` where it would try to print an exception Traceback even if none existed
* Enhancements
* Improvements to the **history** command
* Simplified the display format and made it more similar to **bash**
@@ -10,6 +11,16 @@
* 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 ``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 8dafffaf..96b08f4e 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
@@ -587,7 +597,7 @@ class Cmd(cmd.Cmd):
:param err_color: (optional) color escape to output error with
:param war_color: (optional) color escape to output warning with
"""
- if self.debug:
+ if self.debug and sys.exc_info() != (None, None, None):
import traceback
traceback.print_exc()
@@ -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
@@ -3605,6 +3623,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/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 6c14ff1d..f3ce841d 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -25,9 +25,27 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
Named tuple attributes
----------------------
stdout: str - Output captured from stdout while this command is executing
- stderr: str - Output captured from stderr while this command is executing. None if no error captured
+ stderr: str - Output captured from stderr while this command is executing. None if no error captured.
data - Data returned by the command.
+ Any combination of these fields can be used when developing a scripting API for a given command.
+ By default stdout and stderr will be captured for you. If there is additional command specific data,
+ then write that to cmd2's _last_result member. That becomes the data member of this tuple.
+
+ In some cases, the data member may contain everything needed for a command and storing stdout
+ and stderr might just be a duplication of data that wastes memory. In that case, the StdSim can
+ be told not to store output with its pause_storage member. While this member is True, any output
+ sent to StdSim won't be saved in its buffer.
+
+ The code would look like this:
+ if isinstance(self.stdout, StdSim):
+ self.stdout.pause_storage = True
+
+ if isinstance(sys.stderr, StdSim):
+ sys.stderr.pause_storage = True
+
+ See StdSim class in utils.py for more information
+
NOTE: Named tuples are immutable. So the contents are there for access, not for modification.
"""
def __bool__(self) -> bool:
@@ -67,25 +85,25 @@ class PyscriptBridge(object):
if echo is None:
echo = self.cmd_echo
- copy_stdout = StdSim(sys.stdout, echo)
- copy_stderr = StdSim(sys.stderr, echo)
-
+ # This will be used to capture _cmd2_app.stdout and sys.stdout
copy_cmd_stdout = StdSim(self._cmd2_app.stdout, echo)
+ # This will be used to capture sys.stderr
+ copy_stderr = StdSim(sys.stderr, echo)
+
self._cmd2_app._last_result = None
try:
self._cmd2_app.stdout = copy_cmd_stdout
- with redirect_stdout(copy_stdout):
+ with redirect_stdout(copy_cmd_stdout):
with redirect_stderr(copy_stderr):
# Include a newline in case it's a multiline command
self._cmd2_app.onecmd_plus_hooks(command + '\n')
finally:
self._cmd2_app.stdout = copy_cmd_stdout.inner_stream
- # if stderr is empty, set it to None
- stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
-
- outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
- result = CommandResult(stdout=outbuf, stderr=stderr, data=self._cmd2_app._last_result)
+ # Save the output. If stderr is empty, set it to None.
+ result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
+ stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
+ data=self._cmd2_app._last_result)
return result
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 098ed41d..a8760a65 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -261,28 +261,10 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
class StdSim(object):
- """Class to simulate behavior of sys.stdout or sys.stderr.
-
+ """
+ Class to simulate behavior of sys.stdout or sys.stderr.
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
"""
- class ByteBuf(object):
- """Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
- def __init__(self, inner_stream, echo: bool = False,
- encoding: str = 'utf-8', errors: str = 'replace') -> None:
- self.byte_buf = b''
- self.inner_stream = inner_stream
- self.echo = echo
- self.encoding = encoding
- self.errors = errors
-
- def write(self, b: bytes) -> None:
- """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
- if not isinstance(b, bytes):
- raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
- self.byte_buf += b
- if self.echo:
- self.inner_stream.buffer.write(b)
-
def __init__(self, inner_stream, echo: bool = False,
encoding: str = 'utf-8', errors: str = 'replace') -> None:
"""
@@ -292,17 +274,20 @@ class StdSim(object):
:param encoding: codec for encoding/decoding strings (defaults to utf-8)
:param errors: how to handle encoding/decoding errors (defaults to replace)
"""
- self.buffer = self.ByteBuf(inner_stream, echo)
self.inner_stream = inner_stream
self.echo = echo
self.encoding = encoding
self.errors = errors
+ self.pause_storage = False
+ self.buffer = ByteBuf(self)
def write(self, s: str) -> None:
"""Add str to internal bytes buffer and if echo is True, echo contents to inner stream"""
if not isinstance(s, str):
raise TypeError('write() argument must be str, not {}'.format(type(s)))
- self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
+
+ if not self.pause_storage:
+ self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
if self.echo:
self.inner_stream.write(s)
@@ -337,6 +322,24 @@ class StdSim(object):
return getattr(self.inner_stream, item)
+class ByteBuf(object):
+ """
+ Used by StdSim to write binary data and stores the actual bytes written
+ """
+ def __init__(self, std_sim_instance: StdSim) -> None:
+ self.byte_buf = b''
+ self.std_sim_instance = std_sim_instance
+
+ def write(self, b: bytes) -> None:
+ """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
+ if not isinstance(b, bytes):
+ raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
+ if not self.std_sim_instance.pause_storage:
+ self.byte_buf += b
+ if self.std_sim_instance.echo:
+ self.std_sim_instance.inner_stream.buffer.write(b)
+
+
def unquote_redirection_tokens(args: List[str]) -> None:
"""
Unquote redirection tokens in a list of command-line arguments
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/freefeatures.rst b/docs/freefeatures.rst
index d661e058..1ae3c7ac 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -130,29 +130,13 @@ debugging your application. To prevent users from enabling this ability
manually you'll need to remove ``locals_in_py`` from the ``settable`` dictionary.
The ``app`` object (or your custom name) provides access to application commands
-through either raw commands or through a python API wrapper. For example, any
-application command call be called with ``app("<command>")``. All application
-commands are accessible as python objects and functions matching the command
-name. For example, the following are equivalent:
+through raw commands. For example, any application command call be called with
+``app("<command>")``.
::
>>> app('say --piglatin Blah')
lahBay
- >>> app.say("Blah", piglatin=True)
- lahBay
-
-
-Sub-commands are also supported. The following pairs are equivalent:
-
-::
-
- >>> app('command subcmd1 subcmd2 param1 --myflag --otherflag 3')
- >>> app.command.subcmd1.subcmd2('param1', myflag=True, otherflag=3)
-
- >>> app('command subcmd1 param1 subcmd2 param2 --myflag --otherflag 3')
- >>> app.command.subcmd1('param1').subcmd2('param2', myflag=True, otherflag=3)
-
More Python examples:
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)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 75d4479a..307f69da 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -194,3 +194,25 @@ def test_stdsim_getattr_noexist(stdout_sim):
# Here the StdSim getattr is allowing us to access methods defined by the inner stream
assert not stdout_sim.isatty()
+def test_stdsim_pause_storage(stdout_sim):
+ # Test pausing storage for string data
+ my_str = 'Hello World'
+
+ stdout_sim.pause_storage = False
+ stdout_sim.write(my_str)
+ assert stdout_sim.read() == my_str
+
+ stdout_sim.pause_storage = True
+ stdout_sim.write(my_str)
+ assert stdout_sim.read() == ''
+
+ # Test pausing storage for binary data
+ b_str = b'Hello World'
+
+ stdout_sim.pause_storage = False
+ stdout_sim.buffer.write(b_str)
+ assert stdout_sim.readbytes() == b_str
+
+ stdout_sim.pause_storage = True
+ stdout_sim.buffer.write(b_str)
+ assert stdout_sim.getbytes() == b''