Skip to content

Commit

Permalink
Merge "Add sphinx integration"
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed May 28, 2015
2 parents fd46261 + 7295f78 commit c43bf3b
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 0 deletions.
1 change: 1 addition & 0 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
'sphinx.ext.graphviz',
'sphinx.ext.extlinks',
'oslosphinx',
'stevedore.sphinxext',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Contents:
patterns_enabling
tutorial/index
managers
sphinxext
install
essays/*
history
Expand Down
73 changes: 73 additions & 0 deletions doc/source/sphinxext.rst
Original file line number Diff line number Diff line change
@@ -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 changes: 108 additions & 0 deletions stevedore/sphinxext.py
Original file line number Diff line number Diff line change
@@ -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 changes: 120 additions & 0 deletions stevedore/tests/test_sphinxext.py
Original file line number Diff line number Diff line change
@@ -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,
)

0 comments on commit c43bf3b

Please sign in to comment.