summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoug Hellmann <doug@doughellmann.com>2015-05-16 13:53:35 -0400
committerDoug Hellmann <doug@doughellmann.com>2015-05-27 23:36:04 +0000
commit7295f785f055d1551f4d61b7d1d500bac04dfa9f (patch)
tree8d6cd11dda10c71cb44424aec792eb7bdafe8bfb
parentd4e7ad898eb3963480306cd1df36854cc7c6abd5 (diff)
downloadstevedore-7295f785f055d1551f4d61b7d1d500bac04dfa9f.tar.gz
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
-rw-r--r--doc/source/conf.py1
-rw-r--r--doc/source/index.rst1
-rw-r--r--doc/source/sphinxext.rst73
-rw-r--r--stevedore/sphinxext.py108
-rw-r--r--stevedore/tests/test_sphinxext.py120
5 files changed, 303 insertions, 0 deletions
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 16f953e..c606daa 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -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.
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 884c014..9fa9548 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -21,6 +21,7 @@ Contents:
patterns_enabling
tutorial/index
managers
+ sphinxext
install
essays/*
history
diff --git a/doc/source/sphinxext.rst b/doc/source/sphinxext.rst
new file mode 100644
index 0000000..99f5ea3
--- /dev/null
+++ b/doc/source/sphinxext.rst
@@ -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.
diff --git a/stevedore/sphinxext.py b/stevedore/sphinxext.py
new file mode 100644
index 0000000..524f9c9
--- /dev/null
+++ b/stevedore/sphinxext.py
@@ -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)
diff --git a/stevedore/tests/test_sphinxext.py b/stevedore/tests/test_sphinxext.py
new file mode 100644
index 0000000..60b4794
--- /dev/null
+++ b/stevedore/tests/test_sphinxext.py
@@ -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,
+ )