Skip to content

Commit 290f224

Browse files
authored
Merge pull request #643 from python-cmd2/disable_command
Disable commands
2 parents d9cd632 + f787f47 commit 290f224

File tree

6 files changed

+401
-139
lines changed

6 files changed

+401
-139
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
* Added ``matches_sort_key`` to override the default way tab completion matches are sorted
1414
* Added ``StdSim.pause_storage`` member which when True will cause ``StdSim`` to not save the output sent to it.
1515
See documentation for ``CommandResult`` in ``pyscript_bridge.py`` for reasons pausing the storage can be useful.
16+
* Added ability to disable/enable individual commands and entire categories of commands. When a command
17+
is disabled, it will not show up in the help menu or tab complete. If a user tries to run the command
18+
or call help on it, a command-specific message supplied by the developer will be printed. The following
19+
commands were added to support this feature.
20+
* ``enable_command()``
21+
* ``enable_category()``
22+
* ``disable_command()``
23+
* ``disable_category()``
1624
* Potentially breaking changes
1725
* Made ``cmd2_app`` a positional and required argument of ``AutoCompleter`` since certain functionality now
1826
requires that it can't be ``None``.

cmd2/cmd2.py

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import re
3838
import sys
3939
import threading
40+
from collections import namedtuple
4041
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO
4142

4243
import colorama
@@ -279,6 +280,10 @@ class EmptyStatement(Exception):
279280
pass
280281

281282

283+
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
284+
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])
285+
286+
282287
class Cmd(cmd.Cmd):
283288
"""An easy but powerful framework for writing line-oriented command interpreters.
284289
@@ -521,6 +526,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
521526
# being printed by a command.
522527
self.terminal_lock = threading.RLock()
523528

529+
# Commands that have been disabled from use. This is to support commands that are only available
530+
# during specific states of the application. This dictionary's keys are the command names and its
531+
# values are DisabledCommand objects.
532+
self.disabled_commands = dict()
533+
524534
# ----- Methods related to presenting output to the user -----
525535

526536
@property
@@ -1562,14 +1572,19 @@ def get_all_commands(self) -> List[str]:
15621572
if name.startswith(COMMAND_FUNC_PREFIX) and callable(getattr(self, name))]
15631573

15641574
def get_visible_commands(self) -> List[str]:
1565-
"""Returns a list of commands that have not been hidden."""
1575+
"""Returns a list of commands that have not been hidden or disabled."""
15661576
commands = self.get_all_commands()
15671577

15681578
# Remove the hidden commands
15691579
for name in self.hidden_commands:
15701580
if name in commands:
15711581
commands.remove(name)
15721582

1583+
# Remove the disabled commands
1584+
for name in self.disabled_commands:
1585+
if name in commands:
1586+
commands.remove(name)
1587+
15731588
return commands
15741589

15751590
def get_alias_names(self) -> List[str]:
@@ -1953,7 +1968,7 @@ def cmd_func_name(self, command: str) -> str:
19531968
def onecmd(self, statement: Union[Statement, str]) -> bool:
19541969
""" This executes the actual do_* method for a command.
19551970
1956-
If the command provided doesn't exist, then it executes _default() instead.
1971+
If the command provided doesn't exist, then it executes default() instead.
19571972
19581973
:param statement: intended to be a Statement instance parsed command from the input stream, alternative
19591974
acceptance of a str is present only for backward compatibility with cmd
@@ -1969,8 +1984,9 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
19691984
else:
19701985
func = self.cmd_func(statement.command)
19711986
if func:
1972-
# Since we have a valid command store it in the history
1973-
if statement.command not in self.exclude_from_history:
1987+
# Check to see if this command should be stored in history
1988+
if statement.command not in self.exclude_from_history \
1989+
and statement.command not in self.disabled_commands:
19741990
self.history.append(statement)
19751991

19761992
stop = func(statement)
@@ -3186,13 +3202,15 @@ def do_history(self, args: argparse.Namespace) -> None:
31863202

31873203
# -v must be used alone with no other options
31883204
if args.verbose:
3189-
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
3205+
if args.clear or args.edit or args.output_file or args.run or args.transcript \
3206+
or args.expanded or args.script:
31903207
self.poutput("-v can not be used with any other options")
31913208
self.poutput(self.history_parser.format_usage())
31923209
return
31933210

31943211
# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
3195-
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
3212+
if (args.script or args.expanded) \
3213+
and (args.clear or args.edit or args.output_file or args.run or args.transcript):
31963214
self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t")
31973215
self.poutput(self.history_parser.format_usage())
31983216
return
@@ -3598,6 +3616,95 @@ def set_window_title(self, title: str) -> None: # pragma: no cover
35983616
else:
35993617
raise RuntimeError("another thread holds terminal_lock")
36003618

3619+
def enable_command(self, command: str) -> None:
3620+
"""
3621+
Enable a command by restoring its functions
3622+
:param command: the command being enabled
3623+
"""
3624+
# If the commands is already enabled, then return
3625+
if command not in self.disabled_commands:
3626+
return
3627+
3628+
help_func_name = HELP_FUNC_PREFIX + command
3629+
3630+
# Restore the command and help functions to their original values
3631+
dc = self.disabled_commands[command]
3632+
setattr(self, self.cmd_func_name(command), dc.command_function)
3633+
3634+
if dc.help_function is None:
3635+
delattr(self, help_func_name)
3636+
else:
3637+
setattr(self, help_func_name, dc.help_function)
3638+
3639+
# Remove the disabled command entry
3640+
del self.disabled_commands[command]
3641+
3642+
def enable_category(self, category: str) -> None:
3643+
"""
3644+
Enable an entire category of commands
3645+
:param category: the category to enable
3646+
"""
3647+
for cmd_name in list(self.disabled_commands):
3648+
dc = self.disabled_commands[cmd_name]
3649+
cmd_category = getattr(dc.command_function, HELP_CATEGORY, None)
3650+
if cmd_category is not None and cmd_category == category:
3651+
self.enable_command(cmd_name)
3652+
3653+
def disable_command(self, command: str, message_to_print: str) -> None:
3654+
"""
3655+
Disable a command and overwrite its functions
3656+
:param command: the command being disabled
3657+
:param message_to_print: what to print when this command is run or help is called on it while disabled
3658+
"""
3659+
import functools
3660+
3661+
# If the commands is already disabled, then return
3662+
if command in self.disabled_commands:
3663+
return
3664+
3665+
# Make sure this is an actual command
3666+
command_function = self.cmd_func(command)
3667+
if command_function is None:
3668+
raise AttributeError("{} does not refer to a command".format(command))
3669+
3670+
help_func_name = HELP_FUNC_PREFIX + command
3671+
3672+
# Add the disabled command record
3673+
self.disabled_commands[command] = DisabledCommand(command_function=command_function,
3674+
help_function=getattr(self, help_func_name, None))
3675+
3676+
# Overwrite the command and help functions to print the message
3677+
new_func = functools.partial(self._report_disabled_command_usage, message_to_print=message_to_print)
3678+
setattr(self, self.cmd_func_name(command), new_func)
3679+
setattr(self, help_func_name, new_func)
3680+
3681+
def disable_category(self, category: str, message_to_print: str) -> None:
3682+
"""
3683+
Disable an entire category of commands
3684+
:param category: the category to disable
3685+
:param message_to_print: what to print when anything in this category is run or help is called on it
3686+
while disabled
3687+
"""
3688+
all_commands = self.get_all_commands()
3689+
3690+
for cmd_name in all_commands:
3691+
func = self.cmd_func(cmd_name)
3692+
cmd_category = getattr(func, HELP_CATEGORY, None)
3693+
3694+
# If this command is in the category, then disable it
3695+
if cmd_category is not None and cmd_category == category:
3696+
self.disable_command(cmd_name, message_to_print)
3697+
3698+
# noinspection PyUnusedLocal
3699+
def _report_disabled_command_usage(self, *args, message_to_print: str, **kwargs) -> None:
3700+
"""
3701+
Report when a disabled command has been run or had help called on it
3702+
:param args: not used
3703+
:param message_to_print: the message reporting that the command is disabled
3704+
:param kwargs: not used
3705+
"""
3706+
self.poutput(message_to_print)
3707+
36013708
def cmdloop(self, intro: Optional[str] = None) -> None:
36023709
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
36033710

docs/argument_processing.rst

Lines changed: 0 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -195,138 +195,6 @@ Which yields:
195195
.. _argparse: https://docs.python.org/3/library/argparse.html
196196

197197

198-
Grouping Commands
199-
=================
200-
201-
By default, the ``help`` command displays::
202-
203-
Documented commands (type help <topic>):
204-
========================================
205-
alias findleakers pyscript sessions status vminfo
206-
config help quit set stop which
207-
connect history redeploy shell thread_dump
208-
deploy list resources shortcuts unalias
209-
edit load restart sslconnectorciphers undeploy
210-
expire py serverinfo start version
211-
212-
If you have a large number of commands, you can optionally group your commands into categories.
213-
Here's the output from the example ``help_categories.py``::
214-
215-
Documented commands (type help <topic>):
216-
217-
Application Management
218-
======================
219-
deploy findleakers redeploy sessions stop
220-
expire list restart start undeploy
221-
222-
Connecting
223-
==========
224-
connect which
225-
226-
Server Information
227-
==================
228-
resources serverinfo sslconnectorciphers status thread_dump vminfo
229-
230-
Other
231-
=====
232-
alias edit history py quit shell unalias
233-
config help load pyscript set shortcuts version
234-
235-
236-
There are 2 methods of specifying command categories, using the ``@with_category`` decorator or with the
237-
``categorize()`` function. Once a single command category is detected, the help output switches to a categorized
238-
mode of display. All commands with an explicit category defined default to the category `Other`.
239-
240-
Using the ``@with_category`` decorator::
241-
242-
@with_category(CMD_CAT_CONNECTING)
243-
def do_which(self, _):
244-
"""Which command"""
245-
self.poutput('Which')
246-
247-
Using the ``categorize()`` function:
248-
249-
You can call with a single function::
250-
251-
def do_connect(self, _):
252-
"""Connect command"""
253-
self.poutput('Connect')
254-
255-
# Tag the above command functions under the category Connecting
256-
categorize(do_connect, CMD_CAT_CONNECTING)
257-
258-
Or with an Iterable container of functions::
259-
260-
def do_undeploy(self, _):
261-
"""Undeploy command"""
262-
self.poutput('Undeploy')
263-
264-
def do_stop(self, _):
265-
"""Stop command"""
266-
self.poutput('Stop')
267-
268-
def do_findleakers(self, _):
269-
"""Find Leakers command"""
270-
self.poutput('Find Leakers')
271-
272-
# Tag the above command functions under the category Application Management
273-
categorize((do_undeploy,
274-
do_stop,
275-
do_findleakers), CMD_CAT_APP_MGMT)
276-
277-
The ``help`` command also has a verbose option (``help -v`` or ``help --verbose``) that combines
278-
the help categories with per-command Help Messages::
279-
280-
Documented commands (type help <topic>):
281-
282-
Application Management
283-
================================================================================
284-
deploy Deploy command
285-
expire Expire command
286-
findleakers Find Leakers command
287-
list List command
288-
redeploy Redeploy command
289-
restart usage: restart [-h] {now,later,sometime,whenever}
290-
sessions Sessions command
291-
start Start command
292-
stop Stop command
293-
undeploy Undeploy command
294-
295-
Connecting
296-
================================================================================
297-
connect Connect command
298-
which Which command
299-
300-
Server Information
301-
================================================================================
302-
resources Resources command
303-
serverinfo Server Info command
304-
sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains
305-
multiple lines of help information for the user. Each line of help in a
306-
contiguous set of lines will be printed and aligned in the verbose output
307-
provided with 'help --verbose'
308-
status Status command
309-
thread_dump Thread Dump command
310-
vminfo VM Info command
311-
312-
Other
313-
================================================================================
314-
alias Define or display aliases
315-
config Config command
316-
edit Edit a file in a text editor
317-
help List available commands with "help" or detailed help with "help cmd"
318-
history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg]
319-
load Runs commands in script file that is encoded as either ASCII or UTF-8 text
320-
py Invoke python command, shell, or script
321-
pyscript Runs a python script file inside the console
322-
quit Exits this application
323-
set usage: set [-h] [-a] [-l] [settable [settable ...]]
324-
shell Execute a command as if at the OS prompt
325-
shortcuts Lists shortcuts available
326-
unalias Unsets aliases
327-
version Version command
328-
329-
330198
Receiving an argument list
331199
==========================
332200

0 commit comments

Comments
 (0)