Repository: python-cmd2/cmd2

Issue:
command function type annotations are pulled into command help
in `with_argparser`, when an explicit description is not provided we pull the functions `__doc__` property as the description. It currently pulls the entire __doc__ property including any documentation generation annotations. Would be cleaner to omit annotations.

Example:

```
@with_argparser(foo_args)
def do_foo(args: Namespace) -> None
"""
Does foo stuff

:param args: doc-annotation for args
:raises MyException: some notes about exceptions
"""
```

Results in something like:
```
> help foo
Usage: foo

    Does foo stuff

    :param args: doc-annotation for args
    :raises MyException: some notes about exceptions
```

And would be cleaner like:
```
> help foo
Usage: foo

    Does foo stuff
```


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1224?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) Report
> Merging [#1224](https://codecov.io/gh/python-cmd2/cmd2/pull/1224?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) (00b2a3f) into [master](https://codecov.io/gh/python-cmd2/cmd2/commit/96acc5a36233f17edcb0925ddf4ff382a0fa0f77?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) (96acc5a) will **increase** coverage by `0.00%`.
> The diff coverage is `94.73%`.

```diff
@@           Coverage Diff           @@
##           master    #1224   +/-   ##
=======================================
  Coverage   98.52%   98.52%           
=======================================
  Files          22       22           
  Lines        5695     5698    +3     
=======================================
+ Hits         5611     5614    +3     
  Misses         84       84           
```


| [Impacted Files](https://codecov.io/gh/python-cmd2/cmd2/pull/1224?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) | Coverage Δ | |
|---|---|---|
| [cmd2/utils.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1224/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi91dGlscy5weQ==) | `97.98% <93.75%> (-0.15%)` | :arrow_down: |
| [cmd2/cmd2.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1224/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi9jbWQyLnB5) | `98.20% <100.00%> (+0.03%)` | :arrow_up: |
| [cmd2/decorators.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1224/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi9kZWNvcmF0b3JzLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1224?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1224?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2). Last update [96acc5a...00b2a3f](https://codecov.io/gh/python-cmd2/cmd2/pull/1224?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2).


Patch:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index bd29e122..72b26566 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -139,6 +139,7 @@ from .table_creator import (
 from .utils import (
     Settable,
     get_defining_class,
+    strip_doc_annotations,
 )
 
 # Set up readline
@@ -3808,23 +3809,7 @@ class Cmd(cmd.Cmd):
                         doc = cmd_func.__doc__
 
                     # Attempt to locate the first documentation block
-                    cmd_desc = ''
-                    if doc:
-                        found_first = False
-                        for doc_line in doc.splitlines():
-                            stripped_line = doc_line.strip()
-
-                            # Don't include :param type lines
-                            if stripped_line.startswith(':'):
-                                if found_first:
-                                    break
-                            elif stripped_line:
-                                if found_first:
-                                    cmd_desc += "\n"
-                                cmd_desc += stripped_line
-                                found_first = True
-                            elif found_first:
-                                break
+                    cmd_desc = strip_doc_annotations(doc) if doc else ''
 
                     # Add this command to the table
                     table_row = topic_table.generate_data_row([command, cmd_desc])
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index e1aac3cf..4540af8b 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -29,6 +29,9 @@ from .exceptions import (
 from .parsing import (
     Statement,
 )
+from .utils import (
+    strip_doc_annotations,
+)
 
 if TYPE_CHECKING:  # pragma: no cover
     import cmd2
@@ -384,7 +387,7 @@ def with_argparser(
 
         # If the description has not been set, then use the method docstring if one exists
         if parser.description is None and func.__doc__:
-            parser.description = func.__doc__
+            parser.description = strip_doc_annotations(func.__doc__)
 
         # Set the command's help text as argparser.description (which can be None)
         cmd_wrapper.__doc__ = parser.description
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 855ad23e..5856b41a 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -1228,3 +1228,29 @@ class CustomCompletionSettings:
         """
         self.parser = parser
         self.preserve_quotes = preserve_quotes
+
+
+def strip_doc_annotations(doc: str) -> str:
+    """
+    Strip annotations from a docstring leaving only the text description
+
+    :param doc: documentation string
+    """
+    # Attempt to locate the first documentation block
+    cmd_desc = ''
+    found_first = False
+    for doc_line in doc.splitlines():
+        stripped_line = doc_line.strip()
+
+        # Don't include :param type lines
+        if stripped_line.startswith(':'):
+            if found_first:
+                break
+        elif stripped_line:
+            if found_first:
+                cmd_desc += "\n"
+            cmd_desc += stripped_line
+            found_first = True
+        elif found_first:
+            break
+    return cmd_desc
diff --git a/examples/arg_decorators.py b/examples/arg_decorators.py
index 7b1e2941..3b02835e 100755
--- a/examples/arg_decorators.py
+++ b/examples/arg_decorators.py
@@ -48,7 +48,11 @@ class ArgparsingApp(cmd2.Cmd):
 
     @cmd2.with_argparser(pow_parser)
     def do_pow(self, args: argparse.Namespace) -> None:
-        """Raise an integer to a small integer exponent, either positive or negative"""
+        """
+        Raise an integer to a small integer exponent, either positive or negative
+
+        :param args: argparse arguments
+        """
         self.poutput('{} ** {} == {}'.format(args.base, args.exponent, args.base**args.exponent))
 
 


Test patch:
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index be5e0e72..070b506a 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -35,7 +35,13 @@ class ArgparseApp(cmd2.Cmd):
 
     @cmd2.with_argparser(say_parser)
     def do_say(self, args, *, keyword_arg: Optional[str] = None):
-        """Repeat what you tell me to."""
+        """
+        Repeat what you
+        tell me to.
+
+        :param args: argparse namespace
+        :param keyword_arg: Optional keyword arguments
+        """
         words = []
         for word in args.words:
             if word is None:
@@ -198,7 +204,10 @@ def test_argparse_help_docstring(argparse_app):
     out, err = run_cmd(argparse_app, 'help say')
     assert out[0].startswith('Usage: say')
     assert out[1] == ''
-    assert out[2] == 'Repeat what you tell me to.'
+    assert out[2] == 'Repeat what you'
+    assert out[3] == 'tell me to.'
+    for line in out:
+        assert not line.startswith(':')
 
 
 def test_argparse_help_description(argparse_app):