Switch to sphinxcontrib-pecanwsme for API docs

Move the ceilext code into a separate package
so it can be worked on and reused by other
projects using Pecan and WSME.

The new package is being added to the requirements
list under https://review.openstack.org/29149.

Change-Id: If38a69ed8ef654e75bb29da998fbb731143c506c
Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
This commit is contained in:
Doug Hellmann 2013-05-14 16:46:43 -04:00
parent 45640c55cc
commit b635c826b2
4 changed files with 12 additions and 198 deletions

View File

@ -1,189 +0,0 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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.
"""Sphinx extension for automatically generating API documentation
from Pecan controllers exposed through WSME.
"""
import inspect
from docutils import nodes
from docutils.parsers import rst
from docutils.statemachine import ViewList
from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util.docstrings import prepare_docstring
import wsme.types
def import_object(import_name):
"""Import the named object and return it.
The name should be formatted as package.module:obj.
"""
module_name, expr = import_name.split(':', 1)
mod = __import__(module_name)
mod = reduce(getattr, module_name.split('.')[1:], mod)
globals = __builtins__
if not isinstance(globals, dict):
globals = globals.__dict__
return eval(expr, globals, mod.__dict__)
def http_directive(method, path, content):
"""Build an HTTP directive for documenting a single URL.
:param method: HTTP method ('get', 'post', etc.)
:param path: URL
:param content: Text describing the endpoint.
"""
method = method.lower().strip()
if isinstance(content, basestring):
content = content.splitlines()
yield ''
yield '.. http:{method}:: {path}'.format(**locals())
yield ''
for line in content:
yield ' ' + line
yield ''
def datatypename(datatype):
"""Return the formatted name of the data type.
Derived from wsmeext.sphinxext.datatypename.
"""
if isinstance(datatype, wsme.types.DictType):
return 'dict(%s: %s)' % (datatypename(datatype.key_type),
datatypename(datatype.value_type))
if isinstance(datatype, wsme.types.ArrayType):
return 'list(%s)' % datatypename(datatype.item_type)
if isinstance(datatype, wsme.types.UserType):
return ':class:`%s`' % datatype.name
if isinstance(datatype, wsme.types.Base) or hasattr(datatype, '__name__'):
return ':class:`%s`' % datatype.__name__
return datatype.__name__
class RESTControllerDirective(rst.Directive):
required_arguments = 1
option_spec = {
'webprefix': rst.directives.unchanged,
}
has_content = True
def make_rst_for_method(self, path, method):
docstring = prepare_docstring((method.__doc__ or '').rstrip('\n'))
blank_line = docstring[-1]
docstring = docstring[:-1] # remove blank line appended automatically
funcdef = method._wsme_definition
# Add the parameter type information. Assumes that the
# developer has provided descriptions of the parameters.
for arg in funcdef.arguments:
docstring.append(':type %s: %s' %
(arg.name, datatypename(arg.datatype)))
# Add the return type
if funcdef.return_type:
return_type = datatypename(funcdef.return_type)
docstring.append(':return type: %s' % return_type)
# restore the blank line added as a spacer
docstring.append(blank_line)
directive = http_directive('get', path, docstring)
for line in directive:
yield line
def make_rst_for_controller(self, path_prefix, controller):
env = self.state.document.settings.env
app = env.app
controller_path = path_prefix.rstrip('/') + '/'
# Some of the controllers are instantiated dynamically, so
# we need to look at their constructor arguments to see
# what parameters are needed and include them in the
# URL. For now, we only ever want one at a time.
try:
argspec = inspect.getargspec(controller.__init__)
except TypeError:
# The default __init__ for object is a "slot wrapper" not
# a method, so we can't inspect it. It doesn't take any
# arguments, though, so just knowing that we didn't
# override __init__ helps us build the controller path
# correctly.
pass
else:
if len(argspec[0]) > 1:
first_arg_name = argspec[0][1]
controller_path += '(' + first_arg_name + ')/'
if hasattr(controller, 'get_all') and controller.get_all.exposed:
app.info(' Method: get_all')
for line in self.make_rst_for_method(controller_path,
controller.get_all):
yield line
if hasattr(controller, 'get_one') and controller.get_one.exposed:
app.info(' Method: %s' % controller.get_one)
funcdef = controller.get_one._wsme_definition
first_arg_name = funcdef.arguments[0].name
path = controller_path + '(' + first_arg_name + ')/'
for line in self.make_rst_for_method(
path,
controller.get_one):
yield line
# Look for exposed custom methods
for name in sorted(controller._custom_actions.keys()):
app.info(' Method: %s' % name)
method = getattr(controller, name)
path = controller_path + name + '/'
for line in self.make_rst_for_method(path, method):
yield line
def run(self):
env = self.state.document.settings.env
app = env.app
controller_id = self.arguments[0]
app.info('found root-controller %s' % controller_id)
result = ViewList()
controller = import_object(self.arguments[0])
for line in self.make_rst_for_controller(
self.options.get('webprefix', '/'),
controller):
app.info('ADDING: %r' % line)
result.append(line, '<' + __name__ + '>')
node = nodes.section()
# necessary so that the child nodes get the right source/line set
node.document = self.state.document
nested_parse_with_titles(self.state, result, node)
return node.children
def setup(app):
app.info('Initializing %s' % __name__)
app.add_directive('rest-controller', RESTControllerDirective)

View File

@ -138,15 +138,17 @@ write_autodoc_index()
# Add any Sphinx extension module names here, as strings.
# They can be extensions coming with Sphinx (named 'sphinx.ext.*')
# or your custom ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinxcontrib.autohttp.flask',
'wsmeext.sphinxext',
'sphinx.ext.coverage',
'sphinx.ext.pngmath',
'sphinx.ext.viewcode',
'ceilext.api']
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinxcontrib.autohttp.flask',
'wsmeext.sphinxext',
'sphinx.ext.coverage',
'sphinx.ext.pngmath',
'sphinx.ext.viewcode',
'sphinxcontrib.pecanwsme.rest',
]
wsme_protocols = ['restjson', 'restxml']

View File

@ -16,6 +16,7 @@ swift
netifaces
# Docs Requirements
sphinx
sphinxcontrib-pecanwsme>=0.2
docutils==0.9.1 # for bug 1091333, remove after sphinx >1.1.3 is released.
python-spidermonkey
python-subunit