sphinxext: Generate better usage examples

At the moment, the 'autoprogram-cliff' directive uses the default usage
formatter in its output. This is functional but no more. Take, for
example, the following output, generated for the python-openstackclient
project:

  openstack server migrate [--live <hostname>]
                           [--shared-migration | --block-migration]
                           [--disk-overcommit | --no-disk-overcommit] [--wait]
                           <server>

What we actually want is something like the below, which is not only
more legible but also avoids issues like optional argument flags and
the corresponding values being on separate lines:

  openstack server migrate
      [--live <hostname>]
      [--shared-migration | --block-migration]
      [--disk-overcommit | --no-disk-overcommit]
      [--wait]
      <server>

Do this by manually generating our own usage samples, harnessing a
little of the internal argparse infrastructure.

Change-Id: If4dff4991562da9892f1c06f854143b71111007a
This commit is contained in:
Stephen Finucane 2017-05-30 11:56:25 +01:00
parent ff0c10dbcb
commit 78f188023c
2 changed files with 78 additions and 3 deletions

View File

@ -14,6 +14,7 @@
import argparse
import fnmatch
import re
from docutils import nodes
from docutils.parsers import rst
@ -37,10 +38,30 @@ def _indent(text):
def _format_usage(parser):
"""Get usage without a prefix."""
fmt = argparse.HelpFormatter(parser.prog)
fmt.add_usage(parser.usage, parser._actions,
parser._mutually_exclusive_groups, prefix='')
return fmt.format_help().strip().splitlines()
optionals = parser._get_optional_actions()
positionals = parser._get_positional_actions()
groups = parser._mutually_exclusive_groups
# hacked variant of the regex used by the actual argparse module. Unlike
# that version, this one attempts to group long and short opts with their
# optional arguments ensuring that, for example, '---format <FORMAT>'
# becomes ['--format <FORMAT>'] and not ['--format', '<FORMAT>'].
# Yes, they really do use regexes to break apart and rewrap their help
# string. Don't ask me why.
part_regexp = r'\(.*?\)+|\[.*?\]+|(?:(?:-\w|--\w+)(?:\s+<\w+>)?)|\S+'
opt_usage = fmt._format_actions_usage(optionals, groups)
pos_usage = fmt._format_actions_usage(positionals, groups)
opt_parts = re.findall(part_regexp, opt_usage)
pos_parts = re.findall(part_regexp, pos_usage)
parts = opt_parts + pos_parts
if len(' '.join([parser.prog] + parts)) < 72:
return [' '.join([parser.prog] + parts)]
return [parser.prog] + [_indent(x) for x in parts]
def _format_positional_action(action):

View File

@ -103,3 +103,57 @@ class TestSphinxExtension(base.TestBase):
user name
""").lstrip(), output)
def test_multiple_opts(self):
"""Correctly output multiple opts on separate lines."""
parser = argparse.ArgumentParser(prog='hello-world', add_help=False)
parser.add_argument('name', help='user name')
parser.add_argument('--language', dest='lang',
help='greeting language')
parser.add_argument('--translate', action='store_true',
help='translate to local language')
parser.add_argument('--write-to-var-log-something-or-other',
action='store_true',
help='a long opt to force wrapping')
style_group = parser.add_mutually_exclusive_group(required=True)
style_group.add_argument('--polite', action='store_true',
help='use a polite greeting')
style_group.add_argument('--profane', action='store_true',
help='use a less polite greeting')
output = '\n'.join(sphinxext._format_parser(parser))
self.assertEqual(textwrap.dedent("""
.. program:: hello-world
.. code-block:: shell
hello-world
[--language LANG]
[--translate]
[--write-to-var-log-something-or-other]
(--polite | --profane)
name
.. option:: --language <LANG>
greeting language
.. option:: --translate
translate to local language
.. option:: --write-to-var-log-something-or-other
a long opt to force wrapping
.. option:: --polite
use a polite greeting
.. option:: --profane
use a less polite greeting
.. option:: name
user name
""").lstrip(), output)