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.graphviz',
|
||||||
'sphinx.ext.extlinks',
|
'sphinx.ext.extlinks',
|
||||||
'oslosphinx',
|
'oslosphinx',
|
||||||
|
'stevedore.sphinxext',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
@ -21,6 +21,7 @@ Contents:
|
|||||||
patterns_enabling
|
patterns_enabling
|
||||||
tutorial/index
|
tutorial/index
|
||||||
managers
|
managers
|
||||||
|
sphinxext
|
||||||
install
|
install
|
||||||
essays/*
|
essays/*
|
||||||
history
|
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…
x
Reference in New Issue
Block a user