From 7295f785f055d1551f4d61b7d1d500bac04dfa9f Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sat, 16 May 2015 13:53:35 -0400 Subject: [PATCH] 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 --- doc/source/conf.py | 1 + doc/source/index.rst | 1 + doc/source/sphinxext.rst | 73 ++++++++++++++++++ stevedore/sphinxext.py | 108 +++++++++++++++++++++++++++ stevedore/tests/test_sphinxext.py | 120 ++++++++++++++++++++++++++++++ 5 files changed, 303 insertions(+) create mode 100644 doc/source/sphinxext.rst create mode 100644 stevedore/sphinxext.py create mode 100644 stevedore/tests/test_sphinxext.py 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, + )