Skip to content

Commit fadb8d3

Browse files
authored
Merge pull request #563 from python-cmd2/help_summary
Fixed argparse help summary when no docstring was provided
2 parents 924f8a5 + 38f9da6 commit fadb8d3

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:
@@ -2635,15 +2633,22 @@ def _help_menu(self, verbose: bool=False) -> None:
26352633

26362634
for command in visible_commands:
26372635
func = self.cmd_func(command)
2638-
if command in help_topics or func.__doc__:
2639-
if command in help_topics:
2640-
help_topics.remove(command)
2641-
if hasattr(func, HELP_CATEGORY):
2642-
category = getattr(func, HELP_CATEGORY)
2643-
cmds_cats.setdefault(category, [])
2644-
cmds_cats[category].append(command)
2645-
else:
2646-
cmds_doc.append(command)
2636+
has_help_func = False
2637+
2638+
if command in help_topics:
2639+
# Prevent the command from showing as both a command and help topic in the output
2640+
help_topics.remove(command)
2641+
2642+
# Non-argparse commands can have help_functions for their documentation
2643+
if not hasattr(func, 'argparser'):
2644+
has_help_func = True
2645+
2646+
if hasattr(func, HELP_CATEGORY):
2647+
category = getattr(func, HELP_CATEGORY)
2648+
cmds_cats.setdefault(category, [])
2649+
cmds_cats[category].append(command)
2650+
elif func.__doc__ or has_help_func:
2651+
cmds_doc.append(command)
26472652
else:
26482653
cmds_undoc.append(command)
26492654

@@ -2685,51 +2690,51 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
26852690
if self.ruler:
26862691
self.stdout.write('{:{ruler}<{width}}\n'.format('', ruler=self.ruler, width=80))
26872692

2693+
# Try to get the documentation string for each command
2694+
topics = self.get_help_topics()
2695+
26882696
for command in cmds:
2689-
# Try to get the documentation string
2690-
try:
2691-
# first see if there's a help function implemented
2692-
func = getattr(self, 'help_' + command)
2693-
except AttributeError:
2694-
# Couldn't find a help function
2695-
func = self.cmd_func(command)
2696-
try:
2697-
# Now see if help_summary has been set
2698-
doc = func.help_summary
2699-
except AttributeError:
2700-
# Last, try to directly access the function's doc-string
2701-
doc = func.__doc__
2702-
else:
2703-
# we found the help function
2697+
cmd_func = self.cmd_func(command)
2698+
2699+
# Non-argparse commands can have help_functions for their documentation
2700+
if not hasattr(cmd_func, 'argparser') and command in topics:
2701+
help_func = getattr(self, HELP_FUNC_PREFIX + command)
27042702
result = io.StringIO()
2703+
27052704
# try to redirect system stdout
27062705
with redirect_stdout(result):
27072706
# save our internal stdout
27082707
stdout_orig = self.stdout
27092708
try:
27102709
# redirect our internal stdout
27112710
self.stdout = result
2712-
func()
2711+
help_func()
27132712
finally:
27142713
# restore internal stdout
27152714
self.stdout = stdout_orig
27162715
doc = result.getvalue()
27172716

2717+
else:
2718+
doc = cmd_func.__doc__
2719+
27182720
# Attempt to locate the first documentation block
2719-
doc_block = []
2720-
found_first = False
2721-
for doc_line in doc.splitlines():
2722-
stripped_line = doc_line.strip()
2723-
2724-
# Don't include :param type lines
2725-
if stripped_line.startswith(':'):
2726-
if found_first:
2721+
if not doc:
2722+
doc_block = ['']
2723+
else:
2724+
doc_block = []
2725+
found_first = False
2726+
for doc_line in doc.splitlines():
2727+
stripped_line = doc_line.strip()
2728+
2729+
# Don't include :param type lines
2730+
if stripped_line.startswith(':'):
2731+
if found_first:
2732+
break
2733+
elif stripped_line:
2734+
doc_block.append(stripped_line)
2735+
found_first = True
2736+
elif found_first:
27272737
break
2728-
elif stripped_line:
2729-
doc_block.append(stripped_line)
2730-
found_first = True
2731-
elif found_first:
2732-
break
27332738

27342739
for doc_line in doc_block:
27352740
self.stdout.write('{: <{col_width}}{doc}\n'.format(command,
@@ -3408,7 +3413,7 @@ def do_load(self, args: argparse.Namespace) -> None:
34083413

34093414
@with_argparser(relative_load_parser)
34103415
def do__relative_load(self, args: argparse.Namespace) -> None:
3411-
""""""
3416+
"""Run commands in script file that is encoded as either ASCII or UTF-8 text"""
34123417
file_path = args.file_path
34133418
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
34143419
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)