Repository: python-cmd2/cmd2
Pull request #255 (2018-01-17 18:53:30)

Improved how new argparse-based decorators provide help
Now "help command" and "command -h" provide exactly the same help text.

The function docstring for the "do_*" command sets and overrides the ArgumentParser "description" if the docstring is not empty.


Patch:
diff --git a/cmd2.py b/cmd2.py
index 5d11cb43..378ac097 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -288,13 +288,10 @@ def with_argparser_and_unknown_args(argparser):
         argparser.prog = func.__name__[3:]
 
         # put the help message in the method docstring
-        funcdoc = func.__doc__
-        if funcdoc:
-            funcdoc += '\n'
-        else:
-            # if it's None, make it an empty string
-            funcdoc = ''
-        cmd_wrapper.__doc__ = '{}{}'.format(funcdoc, argparser.format_help())
+        if func.__doc__:
+            argparser.description = func.__doc__
+
+        cmd_wrapper.__doc__ = argparser.format_help()
         return cmd_wrapper
     return arg_decorator
 
@@ -315,13 +312,10 @@ def with_argument_parser(argparser):
         argparser.prog = func.__name__[3:]
 
         # put the help message in the method docstring
-        funcdoc = func.__doc__
-        if funcdoc:
-            funcdoc += '\n'
-        else:
-            # if it's None, make it an empty string
-            funcdoc = ''
-        cmd_wrapper.__doc__ = '{}{}'.format(funcdoc, argparser.format_help())
+        if func.__doc__:
+            argparser.description = func.__doc__
+
+        cmd_wrapper.__doc__ = argparser.format_help()
         return cmd_wrapper
     return arg_decorator
 
@@ -1341,7 +1335,7 @@ class Cmd(cmd.Cmd):
         else:
             raise LookupError("Parameter '%s' not supported (type 'show' for list of parameters)." % param)
 
-    set_parser = argparse.ArgumentParser(description='show or set value of a parameter')
+    set_parser = argparse.ArgumentParser()
     set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
     set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
     set_parser.add_argument('settable', nargs='*', help='[param_name] [value]')
@@ -1693,13 +1687,11 @@ Paths or arguments that contain spaces must be enclosed in quotes
             exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
             embed(banner1=banner, exit_msg=exit_msg)
 
-    history_parser = argparse.ArgumentParser(
-                    description='run, edit, and save previously entered commands',
-                    formatter_class=argparse.RawTextHelpFormatter,
-                )
+    history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
     history_parser_group = history_parser.add_mutually_exclusive_group()
     history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
-    history_parser_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items')
+    history_parser_group.add_argument('-e', '--edit', action='store_true',
+                                      help='edit and then run selected history items')
     history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output to file')
     history_parser.add_argument('-s', '--script', action='store_true', help='script format; no separation lines')
     _history_arg_help = """empty               all history items
@@ -1711,8 +1703,8 @@ a..b, a:b, a:, ..b  items by indices (inclusive)
 
     @with_argument_parser(history_parser)
     def do_history(self, args):
-        # If an argument was supplied, then retrieve partial contents of the
-        # history
+        """View, run, edit, and save previously entered commands."""
+        # If an argument was supplied, then retrieve partial contents of the history
         cowardly_refuse_to_run = False
         if args.arg:
             # If a character indicating a slice is present, retrieve
@@ -1735,7 +1727,8 @@ a..b, a:b, a:, ..b  items by indices (inclusive)
         if args.run:
             if cowardly_refuse_to_run:
                 self.perror("Cowardly refusing to run all previously entered commands.", traceback_war=False)
-                self.perror("If this is what you want to do, specify '1:' as the range of history.", traceback_war=False)
+                self.perror("If this is what you want to do, specify '1:' as the range of history.",
+                            traceback_war=False)
             else:
                 for runme in history:
                     self.pfeedback(runme)
@@ -1744,20 +1737,20 @@ a..b, a:b, a:, ..b  items by indices (inclusive)
         elif args.edit:
             fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
             with os.fdopen(fd, 'w') as fobj:
-                for cmd in history:
-                    fobj.write('{}\n'.format(cmd))
+                for command in history:
+                    fobj.write('{}\n'.format(command))
             try:
                 os.system('"{}" "{}"'.format(self.editor, fname))
                 self.do_load(fname)
-            except:
+            except Exception:
                 raise
             finally:
                 os.remove(fname)
         elif args.output_file:
             try:
                 with open(os.path.expanduser(args.output_file), 'w') as fobj:
-                    for cmd in history:
-                        fobj.write('{}\n'.format(cmd))
+                    for command in history:
+                        fobj.write('{}\n'.format(command))
                 plural = 's' if len(history) > 1 else ''
                 self.pfeedback('{} command{} saved to {}'.format(len(history), plural, args.output_file))
             except Exception as e:
@@ -1770,7 +1763,6 @@ a..b, a:b, a:, ..b  items by indices (inclusive)
                 else:
                     self.poutput(hi.pr())
 
-
     @with_argument_list
     def do_edit(self, arglist):
         """Edit a file or command in a text editor.
@@ -1876,7 +1868,6 @@ Script should contain one command per line, just like command would be typed in
 
         self._script_dir.append(os.path.dirname(expanded_path))
 
-
     @staticmethod
     def is_text_file(file_path):
         """
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index 69d0d7ce..cc08e4e5 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -58,25 +58,26 @@ Help Messages
 
 By default, cmd2 uses the docstring of the command method when a user asks
 for help on the command. When you use the ``@with_argument_parser``
-decorator, the formatted help from the ``argparse.ArgumentParser`` is
-appended to the docstring for the method of that command. With this code::
+decorator, the docstring for the ``do_*`` method is used to set the description for the ``argparse.ArgumentParser`` is
+With this code::
 
    argparser = argparse.ArgumentParser()
-   argparser.add_argument('tag', nargs=1, help='tag')
+   argparser.add_argument('tag', help='tag')
    argparser.add_argument('content', nargs='+', help='content to surround with tag')
    @with_argument_parser(argparser)
    def do_tag(self, args):
       """create a html tag"""
-      self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+      self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
       self.stdout.write('\n')
 
 The ``help tag`` command displays:
 
 .. code-block:: none
 
-   create a html tag
    usage: tag [-h] tag content [content ...]
 
+   create a html tag
+
    positional arguments:
      tag         tag
      content     content to surround with tag
@@ -85,14 +86,15 @@ The ``help tag`` command displays:
      -h, --help  show this help message and exit
 
 
-If you would prefer the short description of your command to come after the usage message, leave the docstring on your method empty, but supply a ``description`` variable to the argument parser::
+If you would prefer you can set the ``description`` while instantiating the ``argparse.ArgumentParser`` and leave the
+docstring on your method empty::
 
    argparser = argparse.ArgumentParser(description='create an html tag')
-   argparser.add_argument('tag', nargs=1, help='tag')
+   argparser.add_argument('tag', help='tag')
    argparser.add_argument('content', nargs='+', help='content to surround with tag')
    @with_argument_parser(argparser)
    def do_tag(self, args):
-      self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+      self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
       self.stdout.write('\n')
 
 Now when the user enters ``help tag`` they see:
@@ -117,11 +119,11 @@ To add additional text to the end of the generated help message, use the ``epilo
       description='create an html tag',
       epilog='This command can not generate tags with no content, like <br/>.'
    )
-   argparser.add_argument('tag', nargs=1, help='tag')
+   argparser.add_argument('tag', help='tag')
    argparser.add_argument('content', nargs='+', help='content to surround with tag')
    @with_argument_parser(argparser)
    def do_tag(self, args):
-      self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+      self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
       self.stdout.write('\n')
 
 Which yields:
diff --git a/examples/argparse_example.py b/examples/argparse_example.py
index ae45411c..9f6548de 100755
--- a/examples/argparse_example.py
+++ b/examples/argparse_example.py
@@ -64,14 +64,14 @@ class CmdLineApp(Cmd):
     do_say = do_speak  # now "say" is a synonym for "speak"
     do_orate = do_speak  # another synonym, but this one takes multi-line input
 
-    tag_parser = argparse.ArgumentParser(description='create a html tag')
-    tag_parser.add_argument('tag', nargs=1, help='tag')
+    tag_parser = argparse.ArgumentParser()
+    tag_parser.add_argument('tag', help='tag')
     tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
 
     @with_argument_parser(tag_parser)
     def do_tag(self, args):
         """create a html tag"""
-        self.poutput('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+        self.poutput('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
 
 
     @with_argument_list
diff --git a/examples/pirate.py b/examples/pirate.py
index 55457e06..dd9fd98c 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -76,7 +76,7 @@ class Pirate(Cmd):
     yo_parser = argparse.ArgumentParser()
     yo_parser.add_argument('--ho', type=int, default=2, help="How often to chant 'ho'")
     yo_parser.add_argument('-c', '--commas', action='store_true', help='Intersperse commas')
-    yo_parser.add_argument('beverage', nargs=1, help='beverage to drink with the chant')
+    yo_parser.add_argument('beverage', help='beverage to drink with the chant')
 
     @with_argument_parser(yo_parser)
     def do_yo(self, args):
@@ -84,7 +84,7 @@ class Pirate(Cmd):
         chant = ['yo'] + ['ho'] * args.ho
         separator = ', ' if args.commas else ' '
         chant = separator.join(chant)
-        print('{0} and a bottle of {1}'.format(chant, args.beverage[0]))
+        print('{0} and a bottle of {1}'.format(chant, args.beverage))
 
 
 if __name__ == '__main__':


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 387322b1..021af193 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -21,7 +21,7 @@ edit  help  history  load  py  pyscript  quit  set  shell  shortcuts
 # Help text for the history command
 HELP_HISTORY = """usage: history [-h] [-r | -e | -o FILE] [-s] [arg]
 
-run, edit, and save previously entered commands
+View, run, edit, and save previously entered commands.
 
 positional arguments:
   arg                   empty               all history items
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index fea9bebb..21e81603 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -37,12 +37,12 @@ class ArgparseApp(cmd2.Cmd):
             self.stdout.write('\n')
 
     tag_parser = argparse.ArgumentParser(description='create a html tag')
-    tag_parser.add_argument('tag', nargs=1, help='tag')
+    tag_parser.add_argument('tag', help='tag')
     tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
 
     @cmd2.with_argument_parser(tag_parser)
     def do_tag(self, args):
-        self.stdout.write('<{0}>{1}</{0}>'.format(args.tag[0], ' '.join(args.content)))
+        self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
         self.stdout.write('\n')
 
     @cmd2.with_argument_list
@@ -140,10 +140,14 @@ def test_argparse_quoted_arguments_posix_multiple(argparse_app):
 
 def test_argparse_help_docstring(argparse_app):
     out = run_cmd(argparse_app, 'help say')
-    assert out[0] == 'Repeat what you tell me to.'
+    assert out[0].startswith('usage: say')
+    assert out[1] == ''
+    assert out[2] == 'Repeat what you tell me to.'
 
 def test_argparse_help_description(argparse_app):
     out = run_cmd(argparse_app, 'help tag')
+    assert out[0].startswith('usage: tag')
+    assert out[1] == ''
     assert out[2] == 'create a html tag'
 
 def test_argparse_prog(argparse_app):