sphinxext: Support cliff application

This commit adds a feature to generate a reference of global
optoions of a cliff application to autoprogram-cliff directive.
If a class path of a cliff application is specified as the argument of
autoprogram-cliff directive, it will be interpreted as a cliff application
and global options of the specified application are rendered.

Change-Id: I20e46521a137ca721fae28f10c5cf75d26069e45
This commit is contained in:
Akihiro Motoki 2017-07-13 01:12:08 +00:00
parent dd60a2a253
commit 9a29859cf2
3 changed files with 145 additions and 41 deletions

View File

@ -14,13 +14,16 @@
import argparse
import fnmatch
import importlib
import re
import sys
from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst import directives
from docutils import statemachine
from cliff import app
from cliff import commandmanager
@ -208,10 +211,42 @@ class AutoprogramCliffDirective(rst.Directive):
required_arguments = 1
option_spec = {
'command': directives.unchanged,
'arguments': directives.unchanged,
'ignored': directives.unchanged,
'application': directives.unchanged,
}
def _get_ignored_opts(self):
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()]
return list(set(global_ignored + local_ignored))
def _drop_ignored_options(self, parser, ignored_opts):
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
def _load_app(self):
mod_str, _sep, class_str = self.arguments[0].rpartition('.')
if not mod_str:
return
try:
importlib.import_module(mod_str)
except ImportError:
return
try:
cliff_app_class = getattr(sys.modules[mod_str], class_str)
except AttributeError:
return
if not issubclass(cliff_app_class, app.App):
return
app_arguments = self.options.get('arguments', '').split()
return cliff_app_class(*app_arguments)
def _load_command(self, manager, command_name):
"""Load a command using an instance of a `CommandManager`."""
try:
@ -222,8 +257,42 @@ class AutoprogramCliffDirective(rst.Directive):
'namespace'.format(
command_name, manager.namespace))
def _generate_nodes(self, title, command_name, command_class,
ignored_opts):
def _load_commands(self):
# TODO(sfinucan): We should probably add this wildcarding functionality
# to the CommandManager itself to allow things like "show me the
# commands like 'foo *'"
command_pattern = self.options.get('command')
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()
return dict((name, self._load_command(manager, name))
for name in commands)
def _generate_app_node(self, app, application_name):
ignored_opts = self._get_ignored_opts()
parser = app.parser
self._drop_ignored_options(parser, ignored_opts)
parser.prog = application_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]
return section.children
def _generate_nodes_per_command(self, title, command_name, command_class,
ignored_opts):
"""Generate the relevant Sphinx nodes.
This doesn't bother using raw docutils nodes as they simply don't offer
@ -244,12 +313,7 @@ class AutoprogramCliffDirective(rst.Directive):
parser = command.get_parser(command_name)
ignored_opts = ignored_opts or []
# Drop any ignored actions
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
self._drop_ignored_options(parser, ignored_opts)
section = nodes.section(
'',
@ -267,42 +331,33 @@ class AutoprogramCliffDirective(rst.Directive):
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()
def _generate_command_nodes(self, commands, application_name):
ignored_opts = self._get_ignored_opts()
output = []
for command_name in sorted(commands):
command_class = self._load_command(manager, command_name)
command_class = commands[command_name]
title = command_name
if application_name:
command_name = ' '.join([application_name, command_name])
output.extend(self._generate_nodes(
output.extend(self._generate_nodes_per_command(
title, command_name, command_class, ignored_opts))
return output
def run(self):
self.env = self.state.document.settings.env
application_name = (self.options.get('application')
or self.env.config.autoprogram_cliff_application)
app = self._load_app()
if app:
return self._generate_app_node(app, application_name)
commands = self._load_commands()
return self._generate_command_nodes(commands, application_name)
def setup(app):
app.add_directive('autoprogram-cliff', AutoprogramCliffDirective)

View File

@ -288,11 +288,23 @@ is provided by :doc:`the cliff Sphinx extension <sphinxext>`.
.. code-block:: rest
.. autoprogram-cliff:: cliffdemo.main.DemoApp
:application: cliffdemo
.. autoprogram-cliff:: cliff.demo
:application: cliffdemo
Output
------
Global Options
~~~~~~~~~~~~~~
.. autoprogram-cliff:: cliffdemo.main.DemoApp
:application: cliffdemo
Command Options
~~~~~~~~~~~~~~~
.. autoprogram-cliff:: cliff.demo
:application: cliffdemo

View File

@ -7,27 +7,55 @@ Usage
cliff supports integration with Sphinx by way of a `Sphinx directives`__.
Preparation
-----------
Before using the :rst:dir:`autoprogram-cliff` directive you must add
`'cliff.sphinxext'` extension module to a list of `extensions` in `conf.py`:
.. code-block:: python
extensions = ['cliff.sphinxext']
extensions = ['cliff.sphinxext']
.. rst:directive:: .. autoprogram-cliff:: namespace
Directive
---------
Automatically document an instance of :py:class:`cliff.command.Command`,
.. rst:directive:: .. autoprogram-cliff:: <namespace> or <app class>
Automatically document an instance of :py:class:`cliff.command.Command`
or :py:class:`cliff.app.App`
including a description, usage summary, and overview of all options.
.. important::
There are two modes in this directive: **command** mode and **app**
mode. The directive takes one required argument and the mode is
determined based on the argument specified.
The **command** mode documents various information of a specified instance of
:py:class:`cliff.command.Command`. The **command** mode takes the namespace
that the command(s) can be found in as the argument. This is generally
defined in the `entry_points` section of either `setup.cfg` or
`setup.py`. You can specify which command(s) should be displayed using
`:command:` option.
.. code-block:: rst
.. autoprogram-cliff:: openstack.compute.v2
:command: server add fixed ip
One argument is required, corresponding to the namespace that the command(s)
can be found in. This is generally defined in the `entry_points` section of
either `setup.cfg` or `setup.py`. Refer to the example_ below for more
information.
The **app** mode documents various information of a specified instance of
:py:class:`cliff.app.App`. The **app** mode takes the python path of the
corresponding class as the argument. In the **app** mode, `:application:`
option is usually specified so that the command name is shown in the
rendered output.
.. code-block:: rst
.. autoprogram-cliff:: cliffdemo.main.DemoApp
:application: cliffdemo
Refer to the example_ below for more information.
In addition, the following directive options can be supplied:
@ -38,6 +66,15 @@ Before using the :rst:dir:`autoprogram-cliff` directive you must add
wildcarding is supported. Refer to the example_ below for more
information.
This option is effective only in the **command** mode.
`:arguments`
The arguments to be passed when the cliff application is instantiated.
Some cliff applications requires arguments when instantiated.
This option can be used to specify such arguments.
This option is effective only in the **app** mode.
`:application:`
The top-level application name, which will be prefixed before all
commands. This option overrides the global option