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:
parent
d4e7ad898e
commit
7295f785f0
@ -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.
|
||||
|
@ -21,6 +21,7 @@ Contents:
|
||||
patterns_enabling
|
||||
tutorial/index
|
||||
managers
|
||||
sphinxext
|
||||
install
|
||||
essays/*
|
||||
history
|
||||
|
73
doc/source/sphinxext.rst
Normal file
73
doc/source/sphinxext.rst
Normal 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
108
stevedore/sphinxext.py
Normal 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)
|
120
stevedore/tests/test_sphinxext.py
Normal file
120
stevedore/tests/test_sphinxext.py
Normal 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,
|
||||
)
|
Loading…
Reference in New Issue
Block a user