Add sphinx integration

Add a restructuredtext directive for documenting a set of plugins with
the needed hooks to make it available is sphinx.

Change-Id: I1a24f9326b4e54174d9dc0ae366315fe29c3ac1b
This commit is contained in:
Doug Hellmann 2015-05-16 13:53:35 -04:00
parent d4e7ad898e
commit 7295f785f0
5 changed files with 303 additions and 0 deletions

View File

@ -32,6 +32,7 @@ extensions = [
'sphinx.ext.graphviz',
'sphinx.ext.extlinks',
'oslosphinx',
'stevedore.sphinxext',
]
# Add any paths that contain templates here, relative to this directory.

View File

@ -21,6 +21,7 @@ Contents:
patterns_enabling
tutorial/index
managers
sphinxext
install
essays/*
history

73
doc/source/sphinxext.rst Normal file
View File

@ -0,0 +1,73 @@
====================
Sphinx Integration
====================
Stevedore includes an extension for integrating with Sphinx to
automatically produce documentation about the supported plugins. To
activate the plugin add ``stevedore.sphinxext`` to the list of
extensions in your ``conf.py``.
.. rst:directive:: .. list-plugins:: namespace
List the plugins in a namespace.
Options:
``detailed``
Flag to switch between simple and detailed output (see
below).
``overline-style``
Character to use to draw line above header,
defaults to none.
``underline-style``
Character to use to draw line below header,
defaults to ``=``.
Simple List
===========
By default, the ``list-plugins`` directive produces a simple list of
plugins in a given namespace including the name and the first line of
the docstring. For example:
::
.. list-plugins:: stevedore.example.formatter
produces
------
.. list-plugins:: stevedore.example.formatter
------
Detailed Lists
==============
Adding the ``detailed`` flag to the directive causes the output to
include a separate subsection for each plugin, with the full docstring
rendered. The section heading level can be controlled using the
``underline-style`` and ``overline-style`` options to fit the results
into the structure of your existing document.
::
.. list-plugins:: stevedore.example.formatter
:detailed:
produces
------
.. list-plugins:: stevedore.example.formatter
:detailed:
:underline-style: -
------
.. note::
Depending on how Sphinx is configured, bad reStructuredText syntax in
the docstrings of the plugins may cause the documentation build to
fail completely when detailed mode is enabled.

108
stevedore/sphinxext.py Normal file
View File

@ -0,0 +1,108 @@
# 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.
from __future__ import unicode_literals
import inspect
from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst import directives
from docutils.statemachine import ViewList
from sphinx.util.nodes import nested_parse_with_titles
from stevedore import extension
def _get_docstring(plugin):
return inspect.getdoc(plugin) or ''
def _simple_list(mgr):
for name in sorted(mgr.names()):
ext = mgr[name]
doc = _get_docstring(ext.plugin) or '\n'
summary = doc.splitlines()[0].strip()
yield('* %s -- %s' % (ext.name, summary),
ext.entry_point.module_name)
def _detailed_list(mgr, over='', under='-'):
for name in sorted(mgr.names()):
ext = mgr[name]
if over:
yield (over * len(ext.name), ext.entry_point.module_name)
yield (ext.name, ext.entry_point.module_name)
if under:
yield (under * len(ext.name), ext.entry_point.module_name)
yield ('\n', ext.entry_point.module_name)
doc = _get_docstring(ext.plugin)
if doc:
yield (doc, ext.entry_point.module_name)
else:
yield ('.. warning:: No documentation found in %s'
% ext.entry_point,
ext.entry_point.module_name)
yield ('\n', ext.entry_point.module_name)
class ListPluginsDirective(rst.Directive):
"""Present a simple list of the plugins in a namespace."""
option_spec = {
'class': directives.class_option,
'detailed': directives.flag,
'overline-style': directives.single_char_or_unicode,
'underline-style': directives.single_char_or_unicode,
}
has_content = True
def run(self):
env = self.state.document.settings.env
app = env.app
namespace = ' '.join(self.content).strip()
app.info('documenting plugins from %r' % namespace)
overline_style = self.options.get('overline-style', '')
underline_style = self.options.get('underline-style', '=')
def report_load_failure(mgr, ep, err):
app.warn(u'Failed to load %s: %s' % (ep.module_name, err))
mgr = extension.ExtensionManager(
namespace,
on_load_failure_callback=report_load_failure,
)
result = ViewList()
if 'detailed' in self.options:
data = _detailed_list(
mgr, over=overline_style, under=underline_style)
else:
data = _simple_list(mgr)
for text, source in data:
for line in text.splitlines():
result.append(line, source)
# Parse what we have into a new section.
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, result, node)
return node.children
def setup(app):
app.info('loading stevedore.sphinxext')
app.add_directive('list-plugins', ListPluginsDirective)

View File

@ -0,0 +1,120 @@
# 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.
"""Tests for the sphinx extension
"""
from __future__ import unicode_literals
from stevedore import extension
from stevedore import sphinxext
from stevedore.tests import utils
import mock
import pkg_resources
def _make_ext(name, docstring):
def inner():
pass
inner.__doc__ = docstring
m1 = mock.Mock(spec=pkg_resources.EntryPoint)
m1.module_name = '%s_module' % name
s = mock.Mock(return_value='ENTRY_POINT(%s)' % name)
m1.__str__ = s
return extension.Extension(name, m1, inner, None)
class TestSphinxExt(utils.TestCase):
def setUp(self):
super(TestSphinxExt, self).setUp()
self.exts = [
_make_ext('test1', 'One-line docstring'),
_make_ext('test2', 'Multi-line docstring\n\nAnother para'),
]
self.em = extension.ExtensionManager.make_test_instance(self.exts)
def test_simple_list(self):
results = list(sphinxext._simple_list(self.em))
self.assertEqual(
[
('* test1 -- One-line docstring', 'test1_module'),
('* test2 -- Multi-line docstring', 'test2_module'),
],
results,
)
def test_simple_list_no_docstring(self):
ext = [_make_ext('nodoc', None)]
em = extension.ExtensionManager.make_test_instance(ext)
results = list(sphinxext._simple_list(em))
self.assertEqual(
[
('* nodoc -- ', 'nodoc_module'),
],
results,
)
def test_detailed_list(self):
results = list(sphinxext._detailed_list(self.em))
self.assertEqual(
[
('test1', 'test1_module'),
('-----', 'test1_module'),
('\n', 'test1_module'),
('One-line docstring', 'test1_module'),
('\n', 'test1_module'),
('test2', 'test2_module'),
('-----', 'test2_module'),
('\n', 'test2_module'),
('Multi-line docstring\n\nAnother para', 'test2_module'),
('\n', 'test2_module'),
],
results,
)
def test_detailed_list_format(self):
results = list(sphinxext._detailed_list(self.em, over='+', under='+'))
self.assertEqual(
[
('+++++', 'test1_module'),
('test1', 'test1_module'),
('+++++', 'test1_module'),
('\n', 'test1_module'),
('One-line docstring', 'test1_module'),
('\n', 'test1_module'),
('+++++', 'test2_module'),
('test2', 'test2_module'),
('+++++', 'test2_module'),
('\n', 'test2_module'),
('Multi-line docstring\n\nAnother para', 'test2_module'),
('\n', 'test2_module'),
],
results,
)
def test_detailed_list_no_docstring(self):
ext = [_make_ext('nodoc', None)]
em = extension.ExtensionManager.make_test_instance(ext)
results = list(sphinxext._detailed_list(em))
self.assertEqual(
[
('nodoc', 'nodoc_module'),
('-----', 'nodoc_module'),
('\n', 'nodoc_module'),
('.. warning:: No documentation found in ENTRY_POINT(nodoc)',
'nodoc_module'),
('\n', 'nodoc_module'),
],
results,
)