summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/cli/neutron-reference.rst2
-rw-r--r--doc/source/conf.py5
-rw-r--r--neutronclient/cliff_sphinxext.py385
3 files changed, 2 insertions, 390 deletions
diff --git a/doc/source/cli/neutron-reference.rst b/doc/source/cli/neutron-reference.rst
index 24359a5..d6640ea 100644
--- a/doc/source/cli/neutron-reference.rst
+++ b/doc/source/cli/neutron-reference.rst
@@ -36,7 +36,7 @@ neutron CLI reference
neutron usage
-------------
-.. cliff-app:: neutronclient.shell.NeutronShell
+.. autoprogram-cliff:: neutronclient.shell.NeutronShell
:application: neutron
:arguments: 2.0
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 1bcb1db..b1c6cee 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -9,10 +9,7 @@ extensions = [
'sphinx.ext.autodoc',
'reno.sphinxext',
'openstackdocstheme',
- # 'cliff.sphinxext',
- # TODO(amotoki): Switch to cliff.sphinxext once cliff bug is fixed.
- # https://bugs.launchpad.net/python-cliff/+bug/1692018
- 'neutronclient.cliff_sphinxext',
+ 'cliff.sphinxext',
]
# openstackdocstheme options
diff --git a/neutronclient/cliff_sphinxext.py b/neutronclient/cliff_sphinxext.py
deleted file mode 100644
index ca4756c..0000000
--- a/neutronclient/cliff_sphinxext.py
+++ /dev/null
@@ -1,385 +0,0 @@
-# Copyright (C) 2017, Red Hat, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import argparse
-import fnmatch
-import re
-
-from docutils import nodes
-from docutils.parsers import rst
-from docutils.parsers.rst import directives
-from docutils import statemachine
-from oslo_utils import importutils
-
-from cliff import commandmanager
-
-
-def _indent(text):
- """Indent by four spaces."""
- prefix = ' ' * 4
-
- def prefixed_lines():
- for line in text.splitlines(True):
- yield (prefix + line if line.strip() else line)
-
- return ''.join(prefixed_lines())
-
-
-def _format_description(parser):
- """Get parser description.
-
- We parse this as reStructuredText, allowing users to embed rich
- information in their help messages if they so choose.
- """
- for line in statemachine.string2lines(
- parser.description, tab_width=4, convert_whitespace=True):
- yield line
-
-
-def _format_usage(parser):
- """Get usage without a prefix."""
- fmt = argparse.HelpFormatter(parser.prog)
-
- 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_epilog(parser):
- """Get parser epilog.
-
- We parse this as reStructuredText, allowing users to embed rich
- information in their help messages if they so choose.
- """
- for line in statemachine.string2lines(
- parser.epilog, tab_width=4, convert_whitespace=True):
- yield line
-
-
-def _format_positional_action(action):
- """Format a positional action."""
- if action.help == argparse.SUPPRESS:
- return
-
- # NOTE(stephenfin): We strip all types of brackets from 'metavar' because
- # the 'option' directive dictates that only option argument names should be
- # surrounded by angle brackets
- yield '.. option:: {}'.format(
- (action.metavar or action.dest).strip('<>[]() '))
- if action.help:
- yield ''
- for line in statemachine.string2lines(
- action.help, tab_width=4, convert_whitespace=True):
- yield _indent(line)
-
-
-def _format_optional_action(action):
- """Format an optional action."""
- if action.help == argparse.SUPPRESS:
- return
-
- if action.nargs == 0:
- yield '.. option:: {}'.format(', '.join(action.option_strings))
- else:
- # TODO(stephenfin): At some point, we may wish to provide more
- # information about the options themselves, for example, if nargs is
- # specified
- option_strings = [' '.join(
- [x, action.metavar or '<{}>'.format(action.dest.upper())])
- for x in action.option_strings]
- yield '.. option:: {}'.format(', '.join(option_strings))
-
- if action.help:
- yield ''
- for line in statemachine.string2lines(
- action.help, tab_width=4, convert_whitespace=True):
- yield _indent(line)
-
-
-def _format_parser(parser):
- """Format the output of an argparse 'ArgumentParser' object.
-
- Given the following parser::
-
- >>> import argparse
- >>> parser = argparse.ArgumentParser(prog='hello-world', \
- description='This is my description.',
- epilog='This is my epilog')
- >>> parser.add_argument('name', help='User name', metavar='<name>')
- >>> parser.add_argument('--language', action='store', dest='lang', \
- help='Greeting language')
-
- Returns the following::
-
- This is my description.
-
- .. program:: hello-world
- .. code:: shell
-
- hello-world [-h] [--language LANG] <name>
-
- .. option:: name
-
- User name
-
- .. option:: --language LANG
-
- Greeting language
-
- .. option:: -h, --help
-
- Show this help message and exit
-
- This is my epilog.
- """
- if parser.description:
- for line in _format_description(parser):
- yield line
- yield ''
-
- yield '.. program:: {}'.format(parser.prog)
-
- yield '.. code-block:: shell'
- yield ''
- for line in _format_usage(parser):
- yield _indent(line)
- yield ''
-
- # In argparse, all arguments and parameters are known as "actions".
- # Optional actions are what would be known as flags or options in other
- # libraries, while positional actions would generally be known as
- # arguments. We present these slightly differently.
-
- for action in parser._get_optional_actions():
- for line in _format_optional_action(action):
- yield line
- yield ''
-
- for action in parser._get_positional_actions():
- for line in _format_positional_action(action):
- yield line
- yield ''
-
- if parser.epilog:
- for line in _format_epilog(parser):
- yield line
- yield ''
-
-
-class AutoprogramCliffDirective(rst.Directive):
- """Auto-document a subclass of `cliff.command.Command`."""
-
- has_content = False
- required_arguments = 1
- option_spec = {
- 'command': directives.unchanged,
- 'ignored': directives.unchanged,
- 'application': directives.unchanged,
- }
-
- def _load_command(self, manager, command_name):
- """Load a command using an instance of a `CommandManager`."""
- try:
- # find_command expects the value of argv so split to emulate that
- return manager.find_command(command_name.split())[0]
- except ValueError:
- raise self.error('"{}" is not a valid command in the "{}" '
- 'namespace'.format(
- command_name, manager.namespace))
-
- def _generate_nodes(self, title, command_name, command_class,
- ignored_opts):
- """Generate the relevant Sphinx nodes.
-
- This is a little funky. Parts of this use raw docutils nodes while
- other parts use reStructuredText and nested parsing. The reason for
- this is simple: it avoids us having to reinvent the wheel. While raw
- docutils nodes are helpful for the simpler elements of the output,
- they don't provide an easy way to use Sphinx's own directives, such as
- the 'option' directive. Refer to [1] for more information.
-
- [1] http://www.sphinx-doc.org/en/stable/extdev/markupapi.html
-
- :param title: Title of command
- :param command_name: Name of command, as used on the command line
- :param command_class: Subclass of :py:class:`cliff.command.Command`
- :param prefix: Prefix to apply before command, if any
- :param ignored_opts: A list of options to exclude from output, if any
- :returns: A list of nested docutil nodes
- """
- command = command_class(None, None)
- parser = command.get_parser(command_name)
- ignored_opts = ignored_opts or []
-
- # Drop the automatically-added help action
- for action in list(parser._actions):
- for option_string in action.option_strings:
- if option_string in ignored_opts:
- del parser._actions[parser._actions.index(action)]
- break
-
- section = nodes.section(
- '',
- nodes.title(text=title),
- ids=[nodes.make_id(title)],
- names=[nodes.fully_normalize_name(title)])
-
- source_name = '<{}>'.format(command.__class__.__name__)
- result = statemachine.ViewList()
-
- for line in _format_parser(parser):
- result.append(line, source_name)
-
- self.state.nested_parse(result, 0, section)
-
- return [section]
-
- def run(self):
- self.env = self.state.document.settings.env
-
- command_pattern = self.options.get('command')
- application_name = (self.options.get('application')
- or self.env.config.autoprogram_cliff_application)
-
- global_ignored = self.env.config.autoprogram_cliff_ignored
- local_ignored = self.options.get('ignored', '')
- local_ignored = [x.strip() for x in local_ignored.split(',')
- if x.strip()]
- ignored_opts = list(set(global_ignored + local_ignored))
-
- # TODO(sfinucan): We should probably add this wildcarding functionality
- # to the CommandManager itself to allow things like "show me the
- # commands like 'foo *'"
- manager = commandmanager.CommandManager(self.arguments[0])
- if command_pattern:
- commands = [x for x in manager.commands
- if fnmatch.fnmatch(x, command_pattern)]
- else:
- commands = manager.commands.keys()
-
- output = []
- for command_name in sorted(commands):
- command_class = self._load_command(manager, command_name)
-
- title = command_name
- if application_name:
- command_name = ' '.join([application_name, command_name])
-
- output.extend(self._generate_nodes(
- title, command_name, command_class, ignored_opts))
-
- return output
-
-
-class CliffAppDirective(rst.Directive):
- """Auto-document a `cliff.app.App`."""
-
- has_content = False
- required_arguments = 1
- option_spec = {
- 'arguments': directives.unchanged,
- 'ignored': directives.unchanged,
- 'application': directives.unchanged,
- }
-
- def _generate_nodes(self, title, app, app_name, ignored_opts):
- """Generate the relevant Sphinx nodes.
-
- This is a little funky. Parts of this use raw docutils nodes while
- other parts use reStructuredText and nested parsing. The reason for
- this is simple: it avoids us having to reinvent the wheel. While raw
- docutils nodes are helpful for the simpler elements of the output,
- they don't provide an easy way to use Sphinx's own directives, such as
- the 'option' directive. Refer to [1] for more information.
-
- [1] http://www.sphinx-doc.org/en/stable/extdev/markupapi.html
-
- :param title: Title of command
- :param app: Subclass of :py:class`cliff.app.App`
- :param app_name: The name of the cliff application.
- This is used as the command name.
- :param ignored_opts: A list of options to exclude from output, if any
- :returns: A list of docutil nodes
- """
- parser = app.parser
- ignored_opts = ignored_opts or []
-
- # Drop the automatically-added help action
- for action in list(parser._actions):
- for option_string in action.option_strings:
- if option_string in ignored_opts:
- del parser._actions[parser._actions.index(action)]
- break
-
- parser.prog = app_name
-
- source_name = '<{}>'.format(app.__class__.__name__)
- result = statemachine.ViewList()
- for line in _format_parser(parser):
- result.append(line, source_name)
-
- section = nodes.section()
- self.state.nested_parse(result, 0, section)
- return section.children
-
- def run(self):
- self.env = self.state.document.settings.env
-
- cliff_app_class = importutils.import_class(self.arguments[0])
- app_arguments = self.options.get('arguments', '').split()
- cliff_app = cliff_app_class(*app_arguments)
-
- application_name = (self.options.get('application')
- or self.env.config.autoprogram_cliff_application)
-
- global_ignored = self.env.config.autoprogram_cliff_ignored
- local_ignored = self.options.get('ignored', '')
- local_ignored = [x.strip() for x in local_ignored.split(',')
- if x.strip()]
- ignored_opts = list(set(global_ignored + local_ignored))
-
- output = []
- title = application_name
- output.extend(self._generate_nodes(
- title, cliff_app, application_name, ignored_opts))
-
- return output
-
-
-def setup(app):
- app.add_directive('autoprogram-cliff', AutoprogramCliffDirective)
- app.add_config_value('autoprogram_cliff_application', '', True)
- app.add_config_value('autoprogram_cliff_ignored', ['--help'], True)
-
- app.add_directive('cliff-app', CliffAppDirective)