Skip to content

Commit 33decb4

Browse files
committed
Added a with_category decorator that can be used to tag a command category.
Changed the detection of with_argparse decorated commands to be less hacky/brittle. Now it tags the function with help_summary. Fixed issue with handling commands that provide a custom help_ function. We can now redirect the output to a string to be formatted with the other commands. Added some documentation explaining the new help categories. Updated unit tests.
1 parent 52bf16c commit 33decb4

File tree

5 files changed

+192
-18
lines changed

5 files changed

+192
-18
lines changed

cmd2.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ def __subclasshook__(cls, C):
7979
return True
8080
return NotImplemented
8181

82-
8382
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
8483
try:
8584
from pyperclip.exceptions import PyperclipException
@@ -113,6 +112,12 @@ def __subclasshook__(cls, C):
113112
else:
114113
from contextlib import redirect_stdout, redirect_stderr
115114

115+
if sys.version_info > (3, 0):
116+
from io import StringIO # Python3
117+
else:
118+
from io import BytesIO as StringIO # Python2
119+
120+
116121
# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
117122
ipython_available = True
118123
try:
@@ -183,6 +188,7 @@ class RlType(Enum):
183188
except ImportError:
184189
pass
185190

191+
186192
__version__ = '0.8.4'
187193

188194
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
@@ -212,6 +218,7 @@ class RlType(Enum):
212218

213219
# optional attribute, when tagged on a function, allows cmd2 to categorize commands
214220
HELP_CATEGORY = 'help_category'
221+
HELP_SUMMARY = 'help_summary'
215222

216223

217224
def categorize(func, category):
@@ -358,6 +365,14 @@ def parse_quoted_string(cmdline):
358365
return lexed_arglist
359366

360367

368+
def with_category(category):
369+
"""A decorator to apply a category to a command function"""
370+
def cat_decorator(func):
371+
categorize(func, category)
372+
return func
373+
return cat_decorator
374+
375+
361376
def with_argument_list(func):
362377
"""A decorator to alter the arguments passed to a do_* cmd2
363378
method. Default passes a string of whatever the user typed.
@@ -396,6 +411,9 @@ def cmd_wrapper(instance, cmdline):
396411
if argparser.description is None and func.__doc__:
397412
argparser.description = func.__doc__
398413

414+
if func.__doc__:
415+
setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)
416+
399417
cmd_wrapper.__doc__ = argparser.format_help()
400418

401419
# Mark this function as having an argparse ArgumentParser (used by do_help)
@@ -435,6 +453,9 @@ def cmd_wrapper(instance, cmdline):
435453
if argparser.description is None and func.__doc__:
436454
argparser.description = func.__doc__
437455

456+
if func.__doc__:
457+
setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)
458+
438459
cmd_wrapper.__doc__ = argparser.format_help()
439460

440461
# Mark this function as having an argparse ArgumentParser (used by do_help)
@@ -2984,21 +3005,44 @@ def _print_topics(self, header, cmds, verbose):
29843005

29853006
help_topics = self.get_help_topics()
29863007
for command in cmds:
3008+
doc = ''
3009+
# Try to get the documentation string
3010+
try:
3011+
# first see if there's a help function implemented
3012+
func = getattr(self, 'help_' + command)
3013+
except AttributeError:
3014+
# Couldn't find a help function
3015+
try:
3016+
# Now see if help_summary has been set
3017+
doc = getattr(self, self._func_named(command)).help_summary
3018+
except AttributeError:
3019+
# Last, try to directly ac cess the function's doc-string
3020+
doc = getattr(self, self._func_named(command)).__doc__
3021+
else:
3022+
# we found the help function
3023+
result = StringIO()
3024+
# try to redirect system stdout
3025+
with redirect_stdout(result):
3026+
# save our internal stdout
3027+
stdout_orig = self.stdout
3028+
try:
3029+
# redirect our internal stdout
3030+
self.stdout = result
3031+
func()
3032+
finally:
3033+
# restore internal stdout
3034+
self.stdout = stdout_orig
3035+
doc = result.getvalue()
3036+
29873037
# Attempt to locate the first documentation block
2988-
doc = getattr(self, self._func_named(command)).__doc__
29893038
doc_block = []
29903039
found_first = False
2991-
in_usage = False
29923040
for doc_line in doc.splitlines():
29933041
str(doc_line).strip()
29943042
if len(doc_line.strip()) > 0:
2995-
if in_usage or doc_line.startswith('usage: '):
2996-
in_usage = True
2997-
continue
29983043
doc_block.append(doc_line.strip())
29993044
found_first = True
30003045
else:
3001-
in_usage = False
30023046
if found_first:
30033047
break
30043048

docs/argument_processing.rst

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,138 @@ Which yields:
160160
This command can not generate tags with no content, like <br/>
161161
162162
163+
Grouping Commands
164+
=================
165+
166+
By default, the ``help`` command displays::
167+
168+
Documented commands (type help <topic>):
169+
========================================
170+
alias findleakers pyscript sessions status vminfo
171+
config help quit set stop which
172+
connect history redeploy shell thread_dump
173+
deploy list resources shortcuts unalias
174+
edit load restart sslconnectorciphers undeploy
175+
expire py serverinfo start version
176+
177+
If you have a large number of commands, you can optionally group your commands into categories.
178+
Here's the output from the example ``help_categories.py``::
179+
180+
Documented commands (type help <topic>):
181+
182+
Application Management
183+
======================
184+
deploy findleakers redeploy sessions stop
185+
expire list restart start undeploy
186+
187+
Connecting
188+
==========
189+
connect which
190+
191+
Server Information
192+
==================
193+
resources serverinfo sslconnectorciphers status thread_dump vminfo
194+
195+
Other
196+
=====
197+
alias edit history py quit shell unalias
198+
config help load pyscript set shortcuts version
199+
200+
201+
There are 2 methods of specifying command categories, using the ``@with_category`` decorator or with the
202+
``categorize()`` function. Once a single command category is detected, the help output switches to a categorized
203+
mode of display. All commands with an explicit category defined default to the category `Other`.
204+
205+
Using the ``@with_category`` decorator::
206+
207+
@with_category(CMD_CAT_CONNECTING)
208+
def do_which(self, _):
209+
"""Which command"""
210+
self.poutput('Which')
211+
212+
Using the ``categorize()`` function:
213+
214+
You can call with a single function::
215+
216+
def do_connect(self, _):
217+
"""Connect command"""
218+
self.poutput('Connect')
219+
220+
# Tag the above command functions under the category Connecting
221+
categorize(do_connect, CMD_CAT_CONNECTING)
222+
223+
Or with an Iterable container of functions::
224+
225+
def do_undeploy(self, _):
226+
"""Undeploy command"""
227+
self.poutput('Undeploy')
228+
229+
def do_stop(self, _):
230+
"""Stop command"""
231+
self.poutput('Stop')
232+
233+
def do_findleakers(self, _):
234+
"""Find Leakers command"""
235+
self.poutput('Find Leakers')
236+
237+
# Tag the above command functions under the category Application Management
238+
categorize((do_undeploy,
239+
do_stop,
240+
do_findleakers), CMD_CAT_APP_MGMT)
241+
242+
The ``help`` command also has a verbose option (``help -v`` or ``help --verbose``) that combines
243+
the help categories with per-command Help Messages::
244+
245+
Documented commands (type help <topic>):
246+
247+
Application Management
248+
================================================================================
249+
deploy Deploy command
250+
expire Expire command
251+
findleakers Find Leakers command
252+
list List command
253+
redeploy Redeploy command
254+
restart usage: restart [-h] {now,later,sometime,whenever}
255+
sessions Sessions command
256+
start Start command
257+
stop Stop command
258+
undeploy Undeploy command
259+
260+
Connecting
261+
================================================================================
262+
connect Connect command
263+
which Which command
264+
265+
Server Information
266+
================================================================================
267+
resources Resources command
268+
serverinfo Server Info command
269+
sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains
270+
multiple lines of help information for the user. Each line of help in a
271+
contiguous set of lines will be printed and aligned in the verbose output
272+
provided with 'help --verbose'
273+
status Status command
274+
thread_dump Thread Dump command
275+
vminfo VM Info command
276+
277+
Other
278+
================================================================================
279+
alias Define or display aliases
280+
config Config command
281+
edit Edit a file in a text editor.
282+
help List available commands with "help" or detailed help with "help cmd".
283+
history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg]
284+
load Runs commands in script file that is encoded as either ASCII or UTF-8 text.
285+
py Invoke python command, shell, or script
286+
pyscript Runs a python script file inside the console
287+
quit Exits this application.
288+
set usage: set [-h] [-a] [-l] [settable [settable ...]]
289+
shell Execute a command as if at the OS prompt.
290+
shortcuts Lists shortcuts (aliases) available.
291+
unalias Unsets aliases
292+
version Version command
293+
294+
163295
Receiving an argument list
164296
==========================
165297

examples/help_categories.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
A sample application for tagging categories on commands.
55
"""
66

7-
from cmd2 import Cmd, categorize, __version__, with_argparser
7+
from cmd2 import Cmd, categorize, __version__, with_argparser, with_category
88
import argparse
99

1010

@@ -24,14 +24,14 @@ def do_connect(self, _):
2424
"""Connect command"""
2525
self.poutput('Connect')
2626

27+
# Tag the above command functions under the category Connecting
28+
categorize(do_connect, CMD_CAT_CONNECTING)
29+
30+
@with_category(CMD_CAT_CONNECTING)
2731
def do_which(self, _):
2832
"""Which command"""
2933
self.poutput('Which')
3034

31-
# Tag the above command functions under the category Connecting
32-
categorize(do_connect, CMD_CAT_CONNECTING)
33-
categorize(do_which, CMD_CAT_CONNECTING)
34-
3535
def do_list(self, _):
3636
"""List command"""
3737
self.poutput('List')
@@ -58,6 +58,7 @@ def do_redeploy(self, _):
5858
help='Specify when to restart')
5959

6060
@with_argparser(restart_parser)
61+
@with_category(CMD_CAT_APP_MGMT)
6162
def do_restart(self, _):
6263
"""Restart command"""
6364
self.poutput('Restart')
@@ -84,7 +85,6 @@ def do_findleakers(self, _):
8485
do_start,
8586
do_sessions,
8687
do_redeploy,
87-
do_restart,
8888
do_expire,
8989
do_undeploy,
9090
do_stop,

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
shell Execute a command as if at the OS prompt.
3535
shortcuts Lists shortcuts (aliases) available.
3636
unalias Unsets aliases
37-
3837
"""
3938

4039
# Help text for the history command

tests/test_cmd2.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_base_argparse_help(base_app, capsys):
5555
# Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
5656
run_cmd(base_app, 'set -h')
5757
out, err = capsys.readouterr()
58-
out1 = normalize(out)
58+
out1 = normalize(str(out))
5959

6060
out2 = run_cmd(base_app, 'help set')
6161

@@ -1080,12 +1080,11 @@ def __init__(self, *args, **kwargs):
10801080
# Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x
10811081
cmd2.Cmd.__init__(self, *args, **kwargs)
10821082

1083+
@cmd2.with_category('Some Category')
10831084
def do_diddly(self, arg):
10841085
"""This command does diddly"""
10851086
pass
10861087

1087-
cmd2.categorize(do_diddly, "Some Category")
1088-
10891088
def do_squat(self, arg):
10901089
"""This docstring help will never be shown because the help_squat method overrides it."""
10911090
pass
@@ -1138,7 +1137,7 @@ def test_help_cat_verbose(helpcat_app):
11381137
Custom Category
11391138
================================================================================
11401139
edit This overrides the edit command and does nothing.
1141-
squat This docstring help will never be shown because the help_squat method overrides it.
1140+
squat This command does diddly squat...
11421141
11431142
Some Category
11441143
================================================================================

0 commit comments

Comments
 (0)