merge with pv/sphinx-work

This commit is contained in:
Georg Brandl 2009-04-13 09:45:56 +02:00
commit b13f61bc42
7 changed files with 464 additions and 198 deletions

View File

@ -6,7 +6,8 @@ import sys, os, re
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.addons.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo',
'sphinx.ext.autosummary']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -6,4 +6,103 @@
.. module:: sphinx.ext.autosummary
:synopsis: Generate autodoc summaries
TBW.
.. versionadded: 0.6
This extension generates function/method/attribute summary lists,
similar to those output eg. by Epydoc and other API doc generation
tools. This is especially useful when your docstrings are long and
detailed, and putting each one of them on a separate page makes
them easier to read.
The :mod:`sphinx.ext.autosummary` extension does this in two parts:
1. There is an :dir:`autosummary` directive for generating summary
listings that contain links to the documented items, and short
summary blurbs extracted from their docstrings.
2. The convenience script :program:`sphinx-autogen` can be used to
generate short "stub" files for the entries listed in the
:dir:`autosummary` directives. These by default contain only the
corresponding :mod:`sphinx.ext.autodoc` directive.
.. directive:: autosummary
Insert a table that contains links to documented items, and a short
summary blurb (the first sentence of the docstring) for each of them.
The :dir:`autosummary` directive can also optionally serve as
a :dir:`toctree` entry for the included items.
For example,::
.. currentmodule:: sphinx
.. autosummary::
environment.BuildEnvironment
util.relative_uri
produces a table like this:
.. currentmodule:: sphinx
.. autosummary::
environment.BuildEnvironment
util.relative_uri
.. currentmodule:: sphinx.ext.autosummary
Autosummary preprocesses the docstrings and signatures with the same
:event:`autodoc-process-docstring` and
:event:`autodoc-process-signature` hooks as *autodoc*.
**Options**
* If you want the :dir:`autosummary` table to also serve as a
:dir:`toctree` entry, use the ``toctree`` option, for example::
.. autosummary::
:toctree: DIRNAME
sphinx.environment.BuildEnvironment
sphinx.util.relative_uri
The ``toctree`` option also signals to the :program:`sphinx-autogen`
script that stub pages should be generated for the entries listed
in this directive. The option accepts a directory name as an
argument; :program:`sphinx-autogen` will by default place its output
in this directory.
* If you don't want the :dir:`autosummary` to show function signatures
in the listing, include the ``nosignatures`` option::
.. autosummary::
:nosignatures:
sphinx.environment.BuildEnvironment
sphinx.util.relative_uri
:program:`sphinx-autogen` -- generate autodoc stub pages
--------------------------------------------------------
The :program:`sphinx-autogen` script can be used to conveniently
generate stub documentation pages for items included in
:dir:`autosummary` listings.
For example, the command::
$ sphinx-autogen -o generated *.rst
will read all :dir:`autosummary` tables in the :file:`*.rst` files
that have the ``:toctree:`` option set, and output corresponding stub
pages in directory ``generated`` for all documented items.
The generated pages by default contain text of the form::
sphinx.util.relative_uri
========================
.. autofunction:: sphinx.util.relative_uri
If the ``-o`` option is not given, the script will place the output
files to the directories specified in the ``:toctree:`` options.

View File

@ -372,8 +372,7 @@ class Documenter(object):
else:
# try to introspect the signature
args = self.format_args()
if args is None:
return ''
retann = self.retann
result = self.env.app.emit_firstresult(

View File

@ -108,6 +108,29 @@ def autosummary_toc_visit_latex(self, node):
def autosummary_noop(self, node):
pass
# -- autodoc integration -------------------------------------------------------
def get_documenter(obj):
"""
Get an autodoc.Documenter class suitable for documenting the given object
"""
import sphinx.ext.autodoc as autodoc
if inspect.isclass(obj):
if issubclass(obj, Exception):
return autodoc.ExceptionDocumenter
return autodoc.ClassDocumenter
elif inspect.ismodule(obj):
return autodoc.ModuleDocumenter
elif inspect.ismethod(obj) or inspect.ismethoddescriptor(obj):
return autodoc.MethodDocumenter
elif (inspect.ismemberdescriptor(obj) or inspect.isgetsetdescriptor(obj)
or inspect.isdatadescriptor(obj)):
return autodoc.AttributeDocumenter
elif inspect.isroutine(obj):
return autodoc.FunctionDocumenter
else:
return autodoc.DataDocumenter
# -- .. autosummary:: ----------------------------------------------------------
@ -127,33 +150,35 @@ class Autosummary(Directive):
'nosignatures': directives.flag,
}
def warn(self, msg):
self.warnings.append(self.state.document.reporter.warning(
msg, line=self.lineno))
def run(self):
names = []
names += [x.strip() for x in self.content if x.strip()]
self.env = env = self.state.document.settings.env
self.genopt = {}
self.warnings = []
table, warnings, real_names = get_autosummary(
names, self.state, 'nosignatures' in self.options)
node = table
names = [x.strip().split()[0] for x in self.content
if x.strip() and re.search(r'^[a-zA-Z_]', x.strip()[0])]
items = self.get_items(names)
nodes = [self.get_table(items)]
env = self.state.document.settings.env
if 'toctree' in self.options:
suffix = env.config.source_suffix
all_docnames = env.found_docs.copy()
dirname = posixpath.dirname(env.docname)
if 'toctree' in self.options:
tree_prefix = self.options['toctree'].strip()
docnames = []
for name in names:
name = real_names.get(name, name)
docname = posixpath.join(tree_prefix, name)
for name, sig, summary, real_name in items:
docname = posixpath.join(tree_prefix, real_name)
if docname.endswith(suffix):
docname = docname[:-len(suffix)]
docname = posixpath.normpath(posixpath.join(dirname, docname))
if docname not in env.found_docs:
warnings.append(self.state.document.reporter.warning(
'toctree references unknown document %r' % docname,
line=self.lineno))
self.warn('toctree references unknown document %r'
% docname)
docnames.append(docname)
tocnode = addnodes.toctree()
@ -163,32 +188,79 @@ class Autosummary(Directive):
tocnode['glob'] = None
tocnode = autosummary_toc('', '', tocnode)
return warnings + [node] + [tocnode]
nodes.append(tocnode)
return self.warnings + nodes
def get_items(self, names):
"""
Try to import the given names, and return a list of
``[(name, signature, summary_string, real_name), ...]``
"""
prefixes = ['']
prefixes.insert(0, self.state.document.settings.env.currmodule)
items = []
for name in names:
try:
obj, real_name = import_by_name(name, prefixes=prefixes)
except ImportError:
self.warn('failed to import %s' % name)
items.append((name, '', '', name))
continue
# NB. using real_name here is important, since Documenters
# don't handle module prefixes slightly differently
documenter = get_documenter(obj)(self, real_name)
if not documenter.parse_name():
self.warn('failed to parse name %s' % real_name)
items.append((name, '', '', real_name))
continue
if not documenter.import_object():
self.warn('failed to import object %s' % real_name)
items.append((name, '', '', real_name))
continue
# -- Grab the signature
sig = documenter.format_signature()
if not sig:
sig = ''
else:
return warnings + [node]
sig = mangle_signature(sig).replace('*', r'\*')
# -- Grab the summary
def get_autosummary(names, state, no_signatures=False):
doc = list(documenter.process_doc(documenter.get_doc()))
while doc and not doc[0].strip():
doc.pop(0)
m = re.search(r"^([A-Z][^A-Z]*?\.\s)", " ".join(doc).strip())
if m:
summary = m.group(1).strip()
elif doc:
summary = doc[0].strip()
else:
summary = ''
items.append((name, sig, summary, real_name))
return items
def get_table(self, items):
"""
Generate a proper table node for autosummary:: directive.
*names* is a list of names of Python objects to be imported and added to the
table. *document* is the Docutils document object.
*items* is a list produced by :meth:`get_items`
"""
document = state.document
real_names = {}
warnings = []
prefixes = ['']
prefixes.insert(0, document.settings.env.currmodule)
table = nodes.table('')
group = nodes.tgroup('', cols=2)
table.append(group)
group.append(nodes.colspec('', colwidth=30))
group.append(nodes.colspec('', colwidth=70))
group.append(nodes.colspec('', colwidth=10))
group.append(nodes.colspec('', colwidth=90))
body = nodes.tbody('')
group.append(body)
@ -198,28 +270,64 @@ def get_autosummary(names, state, no_signatures=False):
node = nodes.paragraph('')
vl = ViewList()
vl.append(text, '<autosummary>')
state.nested_parse(vl, 0, node)
self.state.nested_parse(vl, 0, node)
try:
if isinstance(node[0], nodes.paragraph):
node = node[0]
except IndexError:
pass
row.append(nodes.entry('', node))
body.append(row)
for name in names:
try:
obj, real_name = import_by_name(name, prefixes=prefixes)
except ImportError:
warnings.append(document.reporter.warning(
'failed to import %s' % name))
append_row(':obj:`%s`' % name, '')
continue
real_names[name] = real_name
title = ''
for name, sig, summary, real_name in items:
qualifier = 'obj'
col1 = ':'+qualifier+':`%s <%s>`' % (name, real_name)
col2 = title
if 'nosignatures' not in self.options:
col1 = ':%s:`%s <%s>`\ %s' % (qualifier, name, real_name, sig)
else:
col1 = ':%s:`%s <%s>`' % (qualifier, name, real_name)
col2 = summary
append_row(col1, col2)
return table, warnings, real_names
return table
def mangle_signature(sig, max_chars=30):
"""
Reformat function signature to a more compact form.
"""
sig = re.sub(r"^\((.*)\)$", r"\1", sig) + ", "
r = re.compile(r"(?P<name>[a-zA_Z0-9_*]+)(?P<default>=.*?)?, ")
items = r.findall(sig)
args = []
opts = []
total_len = 4
for name, default in items:
if default:
opts.append(name)
else:
args.append(name)
total_len += len(name) + 2
if total_len > max_chars:
if opts:
opts.append('...')
else:
args.append('...')
break
if opts and args:
sig = ", ".join(args) + "[, " + ", ".join(opts) + "]"
elif opts and not args:
sig = "[" + ", ".join(opts) + "]"
else:
sig = ", ".join(args)
sig = unicode(sig).replace(u" ", u"\u00a0")
return u"(%s)" % sig
# -- Importing items -----------------------------------------------------------
def import_by_name(name, prefixes=[None]):
"""

View File

@ -20,16 +20,31 @@
import os
import re
import sys
import getopt
import optparse
import inspect
import pydoc
from jinja2 import Environment, PackageLoader
from sphinx.ext.autosummary import import_by_name
from sphinx.ext.autosummary import import_by_name, get_documenter
from sphinx.util import ensuredir
# create our own templating environment, for module template only
env = Environment(loader=PackageLoader('sphinx.ext.autosummary', 'templates'))
def main(argv):
usage = """%prog [OPTIONS] SOURCEFILE ..."""
p = optparse.OptionParser(usage.strip())
p.add_option("-o", "--output-dir", action="store", type="string",
dest="output_dir", default=None,
help="Directory to place all output in")
p.add_option("-s", "--suffix", action="store", type="string",
dest="suffix", default="rst",
help="Default suffix for files (default: %default)")
options, args = p.parse_args(argv[1:])
if len(args) < 1:
p.error('no input files given')
generate_autosummary_docs(args, options.output_dir,
"." + options.suffix)
def _simple_info(msg):
@ -38,14 +53,22 @@ def _simple_info(msg):
def _simple_warn(msg):
print >>sys.stderr, 'WARNING: ' + msg
def generate_autosummary_docs(sources, output_dir=None, suffix=None,
#------------------------------------------------------------------------------
# Generating output
#------------------------------------------------------------------------------
# create our own templating environment, for module template only
env = Environment(loader=PackageLoader('sphinx.ext.autosummary', 'templates'))
def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
warn=_simple_warn, info=_simple_info):
info('generating autosummary for: %s' % ', '.join(sources))
if output_dir:
info('writing to %s' % output_dir)
# read
names = {}
for name, loc in get_documented(sources).items():
for name, loc in get_documented_in_files(sources).items():
for (filename, sec_title, keyword, toctree) in loc:
if toctree is not None:
path = os.path.join(os.path.dirname(filename), toctree)
@ -62,7 +85,7 @@ def generate_autosummary_docs(sources, output_dir=None, suffix=None,
warn('failed to import %r: %s' % (name, e))
continue
fn = os.path.join(path, name + (suffix or '.rst'))
fn = os.path.join(path, name + suffix)
# skip it if it exists
if os.path.isfile(fn):
continue
@ -73,17 +96,16 @@ def generate_autosummary_docs(sources, output_dir=None, suffix=None,
if inspect.ismodule(obj):
# XXX replace this with autodoc's API?
tmpl = env.get_template('module')
functions = [getattr(obj, item).__name__
for item in dir(obj)
if inspect.isfunction(getattr(obj, item))]
classes = [getattr(obj, item).__name__
for item in dir(obj)
if inspect.isclass(getattr(obj, item))
and not issubclass(getattr(obj, item), Exception)]
exceptions = [getattr(obj, item).__name__
for item in dir(obj)
if inspect.isclass(getattr(obj, item))
and issubclass(getattr(obj, item), Exception)]
def get_items(mod, typ):
return [getattr(mod, name).__name__
for name in dir(mod)
if get_documenter(getattr(mod,name)).objtype==typ]
functions = get_items(obj, 'function')
classes = get_items(obj, 'class')
exceptions = get_items(obj, 'exception')
rendered = tmpl.render(name=name,
underline='='*len(name),
functions=functions,
@ -96,19 +118,11 @@ def generate_autosummary_docs(sources, output_dir=None, suffix=None,
else:
f.write('%s\n%s\n\n' % (name, '='*len(name)))
if inspect.isclass(obj):
if issubclass(obj, Exception):
f.write(format_modulemember(name, 'autoexception'))
doc = get_documenter(obj)
if doc.objtype in ('method', 'attribute'):
f.write(format_classmember(name, 'auto%s' % doc.objtype))
else:
f.write(format_modulemember(name, 'autoclass'))
elif inspect.ismethod(obj) or inspect.ismethoddescriptor(obj):
f.write(format_classmember(name, 'automethod'))
elif callable(obj):
f.write(format_modulemember(name, 'autofunction'))
elif hasattr(obj, '__get__'):
f.write(format_classmember(name, 'autoattribute'))
else:
f.write(format_modulemember(name, 'autofunction'))
f.write(format_modulemember(name, 'auto%s' % doc.objtype))
finally:
f.close()
@ -125,36 +139,70 @@ def format_classmember(name, directive):
return '.. currentmodule:: %s\n\n.. %s:: %s\n' % (mod, directive, name)
title_underline_re = re.compile('^[-=*_^#]{3,}\s*$')
autodoc_re = re.compile(r'.. auto(function|method|attribute|class|exception'
'|module)::\s*([A-Za-z0-9_.]+)\s*$')
#------------------------------------------------------------------------------
# Finding documented entries in files
#------------------------------------------------------------------------------
def get_documented_in_files(filenames):
"""
Find out what items are documented in source/*.rst
See `get_documented_in_lines`.
"""
documented = {}
for filename in filenames:
f = open(filename, 'r')
lines = f.read().splitlines()
documented.update(get_documented_in_lines(lines, filename=filename))
f.close()
return documented
def get_documented_in_docstring(name, module=None, filename=None):
"""
Find out what items are documented in the given object's docstring.
See `get_documented_in_lines`.
"""
try:
obj, real_name = import_by_name(name)
lines = pydoc.getdoc(obj).splitlines()
return get_documented_in_lines(lines, module=name, filename=filename)
except AttributeError:
pass
except ImportError, e:
print "Failed to import '%s': %s" % (name, e)
return {}
def get_documented_in_lines(lines, module=None, filename=None):
"""
Find out what items are documented in the given lines
Returns
-------
documented : dict of list of (filename, title, keyword, toctree)
Dictionary whose keys are documented names of objects.
The value is a list of locations where the object was documented.
Each location is a tuple of filename, the current section title,
the name of the directive, and the value of the :toctree: argument
(if present) of the directive.
"""
title_underline_re = re.compile("^[-=*_^#]{3,}\s*$")
autodoc_re = re.compile(".. auto(function|method|attribute|class|exception|module)::\s*([A-Za-z0-9_.]+)\s*$")
autosummary_re = re.compile(r'^\.\.\s+autosummary::\s*')
module_re = re.compile(r'^\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$')
autosummary_item_re = re.compile(r'^\s+([_a-zA-Z][a-zA-Z0-9_.]*)\s*')
autosummary_item_re = re.compile(r'^\s+([_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?')
toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$')
def get_documented(filenames):
"""
Find out what items are documented in the given filenames.
Returns a dict of list of (filename, title, keyword, toctree) Keys are
documented names of objects. The value is a list of locations where the
object was documented. Each location is a tuple of filename, the current
section title, the name of the directive, and the value of the :toctree:
argument (if present) of the directive.
"""
documented = {}
for filename in filenames:
current_title = []
last_line = None
toctree = None
current_module = None
current_module = module
in_autosummary = False
f = open(filename, 'r')
for line in f:
for line in lines:
try:
if in_autosummary:
m = toctree_arg_re.match(line)
@ -166,12 +214,10 @@ def get_documented(filenames):
continue # skip options
m = autosummary_item_re.match(line)
if m:
name = m.group(1).strip()
if current_module and \
not name.startswith(current_module + '.'):
name = '%s.%s' % (current_module, name)
if current_module and not name.startswith(current_module + '.'):
name = "%s.%s" % (current_module, name)
documented.setdefault(name, []).append(
(filename, current_title, 'autosummary', toctree))
continue
@ -187,14 +233,14 @@ def get_documented(filenames):
m = autodoc_re.search(line)
if m:
name = m.group(2).strip()
# XXX look in newer generate.py
if current_module and \
not name.startswith(current_module + '.'):
name = '%s.%s' % (current_module, name)
if m.group(1) == 'module':
if m.group(1) == "module":
current_module = name
documented.update(get_documented_in_docstring(
name, filename=filename))
elif current_module and not name.startswith(current_module+'.'):
name = "%s.%s" % (current_module, name)
documented.setdefault(name, []).append(
(filename, current_title, 'auto' + m.group(1), None))
(filename, current_title, "auto" + m.group(1), None))
continue
m = title_underline_re.match(line)
@ -208,31 +254,10 @@ def get_documented(filenames):
continue
finally:
last_line = line
return documented
def main(argv):
usage = 'usage: %s [-o output_dir] [-s suffix] sourcefile ...' % sys.argv[0]
try:
opts, args = getopt.getopt(argv[1:], 'o:s:')
except getopt.error:
print >>sys.stderr, usage
return 1
output_dir = None
suffix = None
for opt, val in opts:
if opt == '-o':
output_dir = val
elif opt == '-s':
suffix = val
if len(args) < 1:
print >>sys.stderr, usage
return 1
generate_autosummary_docs(args, output_dir, suffix)
#------------------------------------------------------------------------------
if __name__ == '__main__':
main(sys.argv)
main()

View File

@ -30,6 +30,7 @@ from sphinx.util.smartypants import educateQuotesLatex
HEADER = r'''%% Generated by Sphinx.
\documentclass[%(papersize)s,%(pointsize)s%(classoptions)s]{%(docclass)s}
%(inputenc)s
%(utf8extra)s
%(fontenc)s
%(babel)s
%(fontpkg)s
@ -136,6 +137,7 @@ class LaTeXTranslator(nodes.NodeVisitor):
'pointsize': '10pt',
'classoptions': '',
'inputenc': '\\usepackage[utf8]{inputenc}',
'utf8extra': '\\DeclareUnicodeCharacter{00A0}{\\nobreakspace}',
'fontenc': '\\usepackage[T1]{fontenc}',
'babel': '\\usepackage{babel}',
'fontpkg': '\\usepackage{times}',

32
tests/test_autosummary.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
test_autosummary
~~~~~~~~~~~~~~~~
Test the autosummary extension.
:copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import string
from util import *
from sphinx.ext.autosummary import mangle_signature
def test_mangle_signature():
TEST = """
() :: ()
(a, b, c, d, e) :: (a, b, c, d, e)
(a, b, c=1, d=2, e=3) :: (a, b[, c, d, e])
(a, b, aaa=1, bbb=1, ccc=1, eee=1, fff=1, ggg=1, hhh=1, iii=1, jjj=1) :: (a, b[, aaa, bbb, ccc, eee, fff, ...])
(a, b, c=(), d=<foo>) :: (a, b[, c, d])
(a, b, c='foobar()', d=123) :: (a, b[, c, d])
"""
TEST = [map(string.strip, x.split("::")) for x in TEST.split("\n")
if '::' in x]
for inp, outp in TEST:
res = mangle_signature(inp).strip().replace(u"\u00a0", " ")
assert res == outp, (u"'%s' -> '%s' != '%s'" % (inp, res, outp))