Skip to content

Commit 418f63a

Browse files
authored
Merge branch 'master' into py_enhancements
2 parents 60bf3aa + fadb8d3 commit 418f63a

File tree

2 files changed

+68
-57
lines changed

2 files changed

+68
-57
lines changed

cmd2/cmd2.py

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,16 @@ def __subclasshook__(cls, C):
120120

121121
# optional attribute, when tagged on a function, allows cmd2 to categorize commands
122122
HELP_CATEGORY = 'help_category'
123-
HELP_SUMMARY = 'help_summary'
124123

125124
INTERNAL_COMMAND_EPILOG = ("Notes:\n"
126125
" This command is for internal use and is not intended to be called from the\n"
127126
" command line.")
128127

129128
# All command functions start with this
130-
COMMAND_PREFIX = 'do_'
129+
COMMAND_FUNC_PREFIX = 'do_'
130+
131+
# All help functions start with this
132+
HELP_FUNC_PREFIX = 'help_'
131133

132134

133135
def categorize(func: Union[Callable, Iterable], category: str) -> None:
@@ -211,16 +213,14 @@ def cmd_wrapper(instance, cmdline):
211213

212214
# argparser defaults the program name to sys.argv[0]
213215
# we want it to be the name of our command
214-
argparser.prog = func.__name__[3:]
216+
argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]
215217

216218
# If the description has not been set, then use the method docstring if one exists
217219
if argparser.description is None and func.__doc__:
218220
argparser.description = func.__doc__
219221

220-
if func.__doc__:
221-
setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)
222-
223-
cmd_wrapper.__doc__ = argparser.format_help()
222+
# Set the command's help text as argparser.description (which can be None)
223+
cmd_wrapper.__doc__ = argparser.description
224224

225225
# Mark this function as having an argparse ArgumentParser
226226
setattr(cmd_wrapper, 'argparser', argparser)
@@ -254,16 +254,14 @@ def cmd_wrapper(instance, cmdline):
254254

255255
# argparser defaults the program name to sys.argv[0]
256256
# we want it to be the name of our command
257-
argparser.prog = func.__name__[3:]
257+
argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]
258258

259259
# If the description has not been set, then use the method docstring if one exists
260260
if argparser.description is None and func.__doc__:
261261
argparser.description = func.__doc__
262262

263-
if func.__doc__:
264-
setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)
265-
266-
cmd_wrapper.__doc__ = argparser.format_help()
263+
# Set the command's help text as argparser.description (which can be None)
264+
cmd_wrapper.__doc__ = argparser.description
267265

268266
# Mark this function as having an argparse ArgumentParser
269267
setattr(cmd_wrapper, 'argparser', argparser)
@@ -1606,8 +1604,8 @@ def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int,
16061604

16071605
def get_all_commands(self) -> List[str]:
16081606
"""Returns a list of all commands."""
1609-
return [name[len(COMMAND_PREFIX):] for name in self.get_names()
1610-
if name.startswith(COMMAND_PREFIX) and callable(getattr(self, name))]
1607+
return [name[len(COMMAND_FUNC_PREFIX):] for name in self.get_names()
1608+
if name.startswith(COMMAND_FUNC_PREFIX) and callable(getattr(self, name))]
16111609

16121610
def get_visible_commands(self) -> List[str]:
16131611
"""Returns a list of commands that have not been hidden."""
@@ -1637,8 +1635,8 @@ def get_commands_aliases_and_macros_for_completion(self) -> List[str]:
16371635

16381636
def get_help_topics(self) -> List[str]:
16391637
""" Returns a list of help topics """
1640-
return [name[5:] for name in self.get_names()
1641-
if name.startswith('help_') and callable(getattr(self, name))]
1638+
return [name[len(HELP_FUNC_PREFIX):] for name in self.get_names()
1639+
if name.startswith(HELP_FUNC_PREFIX) and callable(getattr(self, name))]
16421640

16431641
# noinspection PyUnusedLocal
16441642
def sigint_handler(self, signum: int, frame) -> None:
@@ -1985,7 +1983,7 @@ def cmd_func_name(self, command: str) -> str:
19851983
:param command: command to look up method name which implements it
19861984
:return: method name which implements the given command
19871985
"""
1988-
target = COMMAND_PREFIX + command
1986+
target = COMMAND_FUNC_PREFIX + command
19891987
return target if callable(getattr(self, target, None)) else ''
19901988

19911989
def onecmd(self, statement: Union[Statement, str]) -> bool:
@@ -2620,15 +2618,22 @@ def _help_menu(self, verbose: bool=False) -> None:
26202618

26212619
for command in visible_commands:
26222620
func = self.cmd_func(command)
2623-
if command in help_topics or func.__doc__:
2624-
if command in help_topics:
2625-
help_topics.remove(command)
2626-
if hasattr(func, HELP_CATEGORY):
2627-
category = getattr(func, HELP_CATEGORY)
2628-
cmds_cats.setdefault(category, [])
2629-
cmds_cats[category].append(command)
2630-
else:
2631-
cmds_doc.append(command)
2621+
has_help_func = False
2622+
2623+
if command in help_topics:
2624+
# Prevent the command from showing as both a command and help topic in the output
2625+
help_topics.remove(command)
2626+
2627+
# Non-argparse commands can have help_functions for their documentation
2628+
if not hasattr(func, 'argparser'):
2629+
has_help_func = True
2630+
2631+
if hasattr(func, HELP_CATEGORY):
2632+
category = getattr(func, HELP_CATEGORY)
2633+
cmds_cats.setdefault(category, [])
2634+
cmds_cats[category].append(command)
2635+
elif func.__doc__ or has_help_func:
2636+
cmds_doc.append(command)
26322637
else:
26332638
cmds_undoc.append(command)
26342639

@@ -2670,51 +2675,51 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
26702675
if self.ruler:
26712676
self.stdout.write('{:{ruler}<{width}}\n'.format('', ruler=self.ruler, width=80))
26722677

2678+
# Try to get the documentation string for each command
2679+
topics = self.get_help_topics()
2680+
26732681
for command in cmds:
2674-
# Try to get the documentation string
2675-
try:
2676-
# first see if there's a help function implemented
2677-
func = getattr(self, 'help_' + command)
2678-
except AttributeError:
2679-
# Couldn't find a help function
2680-
func = self.cmd_func(command)
2681-
try:
2682-
# Now see if help_summary has been set
2683-
doc = func.help_summary
2684-
except AttributeError:
2685-
# Last, try to directly access the function's doc-string
2686-
doc = func.__doc__
2687-
else:
2688-
# we found the help function
2682+
cmd_func = self.cmd_func(command)
2683+
2684+
# Non-argparse commands can have help_functions for their documentation
2685+
if not hasattr(cmd_func, 'argparser') and command in topics:
2686+
help_func = getattr(self, HELP_FUNC_PREFIX + command)
26892687
result = io.StringIO()
2688+
26902689
# try to redirect system stdout
26912690
with redirect_stdout(result):
26922691
# save our internal stdout
26932692
stdout_orig = self.stdout
26942693
try:
26952694
# redirect our internal stdout
26962695
self.stdout = result
2697-
func()
2696+
help_func()
26982697
finally:
26992698
# restore internal stdout
27002699
self.stdout = stdout_orig
27012700
doc = result.getvalue()
27022701

2702+
else:
2703+
doc = cmd_func.__doc__
2704+
27032705
# Attempt to locate the first documentation block
2704-
doc_block = []
2705-
found_first = False
2706-
for doc_line in doc.splitlines():
2707-
stripped_line = doc_line.strip()
2708-
2709-
# Don't include :param type lines
2710-
if stripped_line.startswith(':'):
2711-
if found_first:
2706+
if not doc:
2707+
doc_block = ['']
2708+
else:
2709+
doc_block = []
2710+
found_first = False
2711+
for doc_line in doc.splitlines():
2712+
stripped_line = doc_line.strip()
2713+
2714+
# Don't include :param type lines
2715+
if stripped_line.startswith(':'):
2716+
if found_first:
2717+
break
2718+
elif stripped_line:
2719+
doc_block.append(stripped_line)
2720+
found_first = True
2721+
elif found_first:
27122722
break
2713-
elif stripped_line:
2714-
doc_block.append(stripped_line)
2715-
found_first = True
2716-
elif found_first:
2717-
break
27182723

27192724
for doc_line in doc_block:
27202725
self.stdout.write('{: <{col_width}}{doc}\n'.format(command,
@@ -3404,7 +3409,7 @@ def do_load(self, args: argparse.Namespace) -> None:
34043409

34053410
@with_argparser(relative_load_parser)
34063411
def do__relative_load(self, args: argparse.Namespace) -> None:
3407-
""""""
3412+
"""Run commands in script file that is encoded as either ASCII or UTF-8 text"""
34083413
file_path = args.file_path
34093414
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
34103415
relative_path = os.path.join(self._current_script_dir or '', file_path)

tests/test_cmd2.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,11 @@ def do_diddly(self, arg):
12361236
"""This command does diddly"""
12371237
pass
12381238

1239+
# This command will be in the "Some Category" section of the help menu even though it has no docstring
1240+
@cmd2.with_category("Some Category")
1241+
def do_cat_nodoc(self, arg):
1242+
pass
1243+
12391244
def do_squat(self, arg):
12401245
"""This docstring help will never be shown because the help_squat method overrides it."""
12411246
pass
@@ -1269,7 +1274,7 @@ def test_help_cat_base(helpcat_app):
12691274
12701275
Some Category
12711276
=============
1272-
diddly
1277+
cat_nodoc diddly
12731278
12741279
Other
12751280
=====
@@ -1292,6 +1297,7 @@ def test_help_cat_verbose(helpcat_app):
12921297
12931298
Some Category
12941299
================================================================================
1300+
cat_nodoc
12951301
diddly This command does diddly
12961302
12971303
Other

0 commit comments

Comments
 (0)