Finish new doc field handling implementation.

This commit is contained in:
Georg Brandl 2010-01-17 12:05:44 +01:00
parent c00fd08aa9
commit c98236bc61
7 changed files with 294 additions and 333 deletions

View File

@ -7,6 +7,9 @@ Release 1.0 (in development)
* Support for docutils 0.4 has been removed.
* New more compact doc field syntax is now recognized:
``:param type name: description``.
* Added the ``viewcode`` extension.
* Added ``html-collect-pages`` event.

View File

@ -292,23 +292,3 @@ The special files are located in the root output directory. They are:
Unlike the other pickle files this pickle file requires that the sphinx
module is available on unpickling.
.. class:: Blah
:param app: the Sphinx application object
:type app: sphinx.builders.Builder
:param what: the type of the object which the docstring belongs to (one of
``"module"``, ``"class"``, ``"exception"``, ``"function"``, ``"method"``,
``"attribute"``)
:param name: the fully qualified name of the object
:param obj: the object itself
:param options: the options given to the directive: an object with attributes
``inherited_members``, ``undoc_members``, ``show_inheritance`` and
``noindex`` that are true if the flag option of same name was given to the
auto directive
:param lines: the lines of the docstring, see above
:raises foo: why?
:raises sphinx.builders.Builder: why not...
:returns: me
:rtype: sphinx.builders.Builder

View File

@ -7,7 +7,7 @@ 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',
'sphinx.ext.autosummary', 'sphinx.ext.viewcode']
'sphinx.ext.autosummary']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -11,12 +11,11 @@
import re
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.parsers.rst.directives import images
from sphinx import addnodes
from sphinx.locale import l_
from sphinx.util.docfields import DocFieldTransformer
# import and register directives
from sphinx.directives.code import *
@ -36,179 +35,6 @@ except AttributeError:
strip_backslash_re = re.compile(r'\\(?=[^\\])')
def _is_only_paragraph(node):
"""True if the node only contains one paragraph (and system messages)."""
if len(node) == 0:
return False
elif len(node) > 1:
for subnode in node[1:]:
if not isinstance(subnode, nodes.system_message):
return False
if isinstance(node[0], nodes.paragraph):
return True
return False
class FieldType(object):
def __init__(self, name, names=(), label=None, grouplabel=None,
objtype=None, has_arg=True):
self.name = name
self.names = names
self.label = label
self.grouplabel = grouplabel
self.objtype = objtype
self.has_arg = has_arg
class TypedFieldType(FieldType):
def __init__(self, name, names=(), typefields=(), label=None,
grouplabel=None, objtype=None, has_arg=True):
FieldType.__init__(self, name, names, label,
grouplabel, objtype, has_arg)
self.typefields = typefields
class DocFieldTransformer(object):
"""
Transforms field lists in "doc field" syntax into better-looking
equivalents, using the field type definitions given on a domain.
"""
def __init__(self, directive):
self.domain = directive.domain
if not hasattr(directive, '_doc_field_type_map'):
directive._doc_field_type_map = \
self.preprocess_fieldtypes(directive.doc_field_types)
self.typemap = directive._doc_field_type_map
def preprocess_fieldtypes(self, types):
typemap = {}
for fieldtype in types:
for name in fieldtype.names:
typemap[name] = fieldtype, 0
if isinstance(fieldtype, TypedFieldType):
for name in fieldtype.typefields:
typemap[name] = fieldtype, 1
return typemap
def transform_all(self, node):
"""Transform all field list children of a node."""
# don't traverse, only handle field lists that are immediate children
for child in node:
if isinstance(child, nodes.field_list):
self.transform(child)
def transform(self, node):
"""Transform a single field list *node*."""
typemap = self.typemap
entries = []
groupindices = {}
types = {}
for field in node:
fieldname, fieldbody = field
try:
fieldtype, arg = fieldname.astext().split(None, 1)
except ValueError:
# argument-less field type?
fieldtype, arg = fieldname.astext(), ''
typedesc, is_typefield = typemap.get(fieldtype, (None, None))
if typedesc is None or \
typedesc.has_arg != bool(arg):
# capitalize field name and be done with it
new_fieldname = fieldtype.capitalize() + ' ' + arg
fieldname[0] = nodes.Text(new_fieldname)
entries.append(field)
continue
typename = typedesc.name
if _is_only_paragraph(fieldbody):
content = fieldbody.children[0].children
else:
content = fieldbody.children
if is_typefield:
types.setdefault(typename, {})[arg] = content
continue
# support syntax like ``:param type name:``
try:
argtype, argname = arg.split(None, 1)
except ValueError:
pass
else:
types.setdefault(typename, {})[arg] = nodes.Text(argtype)
arg = argname
if typedesc.grouplabel:
if typename in groupindices:
group = entries[groupindices[typename]]
else:
group = [typedesc]
groupindices[typename] = len(entries)
entries.append(group)
group.append((arg, content))
else:
entries.append((typedesc, arg, content))
# now that all entries are collected, construct the new field list
new_list = nodes.field_list()
for entry in entries:
if isinstance(entry, nodes.field):
# pass-through old field
new_list += entry
elif isinstance(entry, tuple):
# single entry
entrytype, arg, content = entry
fieldname = nodes.field_name()
para = nodes.paragraph('', entrytype.label)
if arg:
para += nodes.Text(' ')
if entrytype.objtype:
para += addnodes.pending_xref(
'', arg, refdomain=self.domain,
reftype=entrytype.objtype, refexplicit=False,
reftarget=arg)
else:
para += nodes.Text(arg)
fieldname += para
fieldbody = nodes.field_body('', nodes.paragraph('', *content))
new_list += nodes.field('', fieldname, fieldbody)
else:
# group entry
grouptype = entry[0]
groupitems = entry[1:]
grouptypes = types.get(grouptype.name, {})
fieldname = nodes.field_name()
fieldname += nodes.paragraph('', grouptype.grouplabel)
bullets = nodes.bullet_list()
for name, content in groupitems:
par = nodes.paragraph()
par += nodes.emphasis('', name)
if name in grouptypes:
typenodes = grouptypes[name]
par += nodes.Text(' (')
if grouptype.objtype and len(typenodes) == 1:
typename = typenodes[0].astext()
par += addnodes.pending_xref(
'', refdomain=self.domain,
reftype=grouptype.objtype, refexplicit=False,
reftarget=typename)
par[-1] += nodes.emphasis('', typename)
else:
par += typenodes
par += nodes.Text(')')
par += nodes.Text(' -- ')
par += content
bullets += nodes.list_item('', par)
fieldbody = nodes.field_body('', bullets)
new_list += nodes.field('', fieldname, fieldbody)
node.replace_self(new_list)
class ObjectDescription(Directive):
"""
Directive to describe a class, function or similar object. Not used
@ -224,139 +50,8 @@ class ObjectDescription(Directive):
'module': directives.unchanged,
}
doc_field_types = [
TypedFieldType('parameter',
names=('param', 'parameter', 'arg', 'argument',
'keyword', 'kwarg', 'kwparam', 'type'),
label=l_('Parameter'), grouplabel=l_('Parameters'),
objtype='obj', typefields=('type',)),
TypedFieldType('variable', names=('var', 'ivar', 'cvar'), objtype='obj',
label=l_('Variable'), grouplabel=l_('Variables')),
FieldType('exceptions', names=('raises', 'raise', 'exception', 'except'),
label=l_('Raises'), grouplabel=l_('Raises'), objtype='exc'),
FieldType('returnvalue', names=('returns', 'return'),
label=l_('Returns'), has_arg=False),
FieldType('returntype', names=('rtype',),
label=l_('Return type'), has_arg=False),
]
# XXX make this more domain specific
doc_fields_with_arg = {
'param': '%param',
'parameter': '%param',
'arg': '%param',
'argument': '%param',
'keyword': '%param',
'kwarg': '%param',
'kwparam': '%param',
'type': '%type',
'raises': l_('Raises'),
'raise': l_('Raises'),
'exception': l_('Raises'),
'except': l_('Raises'),
'var': l_('Variable'),
'ivar': l_('Variable'),
'cvar': l_('Variable'),
'returns': l_('Returns'),
'return': l_('Returns'),
}
doc_fields_with_linked_arg = ('raises', 'raise', 'exception', 'except')
doc_fields_without_arg = {
'returns': l_('Returns'),
'return': l_('Returns'),
'rtype': l_('Return type'),
}
# XXX refactor this
def handle_doc_fields(self, node):
"""
Convert field lists with known keys inside the description content into
better-looking equivalents.
"""
# don't traverse, only handle field lists that are immediate children
for child in node.children:
if not isinstance(child, nodes.field_list):
continue
params = []
pfield = None
param_nodes = {}
param_types = {}
new_list = nodes.field_list()
for field in child:
fname, fbody = field
try:
typ, obj = fname.astext().split(None, 1)
typdesc = self.doc_fields_with_arg[typ]
if _is_only_paragraph(fbody):
children = fbody.children[0].children
else:
children = fbody.children
if typdesc == '%param':
if not params:
# add the field that later gets all the parameters
pfield = nodes.field()
new_list += pfield
dlitem = nodes.list_item()
dlpar = nodes.paragraph()
dlpar += nodes.emphasis(obj, obj)
dlpar += nodes.Text(' -- ', ' -- ')
dlpar += children
param_nodes[obj] = dlpar
dlitem += dlpar
params.append(dlitem)
elif typdesc == '%type':
typenodes = fbody.children
if _is_only_paragraph(fbody):
typenodes = ([nodes.Text(' (')] +
typenodes[0].children +
[nodes.Text(')')])
param_types[obj] = typenodes
else:
fieldname = typdesc + ' '
nfield = nodes.field()
nfieldname = nodes.field_name(fieldname, fieldname)
nfield += nfieldname
node = nfieldname
if typ in self.doc_fields_with_linked_arg:
# XXX currmodule/currclass
node = addnodes.pending_xref(
obj, reftype='obj', refexplicit=False,
reftarget=obj)
#, modname=self.env.currmodule
#, classname=self.env.currclass
nfieldname += node
node += nodes.Text(obj, obj)
nfield += nodes.field_body()
nfield[1] += fbody.children
new_list += nfield
except (KeyError, ValueError):
fnametext = fname.astext()
try:
typ = self.doc_fields_without_arg[fnametext]
except KeyError:
# at least capitalize the field name
typ = fnametext.capitalize()
fname[0] = nodes.Text(typ)
new_list += field
if params:
if len(params) == 1:
pfield += nodes.field_name('', _('Parameter'))
pfield += nodes.field_body()
pfield[1] += params[0][0]
else:
pfield += nodes.field_name('', _('Parameters'))
pfield += nodes.field_body()
pfield[1] += nodes.bullet_list()
pfield[1][0].extend(params)
for param, type in param_types.iteritems():
if param in param_nodes:
param_nodes[param][1:1] = type
child.replace_self(new_list)
# types of doc fields that this directive handles, see sphinx.util.docfields
doc_field_types = []
def get_signatures(self):
"""
@ -422,7 +117,7 @@ class ObjectDescription(Directive):
# this is strictly domain-specific (i.e. no assumptions may
# be made in this base class)
name = self.handle_signature(sig, signode)
except ValueError, err:
except ValueError:
# signature parsing failed
signode.clear()
signode += addnodes.desc_name(sig, sig)
@ -463,7 +158,7 @@ class DefaultDomain(Directive):
def run(self):
env = self.state.document.settings.env
domain_name = arguments[0]
domain_name = self.arguments[0]
env.doc_read_data['default_domain'] = env.domains.get(domain_name)

View File

@ -13,7 +13,6 @@ import re
import string
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.roles import XRefRole
@ -21,7 +20,7 @@ from sphinx.locale import l_
from sphinx.domains import Domain, ObjType
from sphinx.directives import ObjectDescription
from sphinx.util import make_refnode
from sphinx.util.compat import Directive
from sphinx.util.docfields import Field, TypedField
# RE to split at word boundaries
@ -48,6 +47,16 @@ class CObject(ObjectDescription):
Description of a C language object.
"""
doc_field_types = [
TypedField('parameter', label=l_('Parameters'),
names=('param', 'parameter', 'arg', 'argument'),
typerolename='obj', typenames=('type',)),
Field('returnvalue', label=l_('Returns'), has_arg=False,
names=('returns', 'return')),
Field('returntype', label=l_('Return type'), has_arg=False,
names=('rtype',), rolename='obj'),
]
# These C types aren't described anywhere, so don't try to create
# a cross-reference to them
stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct'))

View File

@ -21,6 +21,7 @@ from sphinx.domains import Domain, ObjType
from sphinx.directives import ObjectDescription
from sphinx.util import make_refnode
from sphinx.util.compat import Directive
from sphinx.util.docfields import Field, GroupedField, TypedField
# REs for Python signatures
@ -40,6 +41,22 @@ class PyObject(ObjectDescription):
Description of a general Python object.
"""
doc_field_types = [
TypedField('parameter', label=l_('Parameters'),
names=('param', 'parameter', 'arg', 'argument',
'keyword', 'kwarg', 'kwparam'),
typerolename='obj', typenames=('type',)),
TypedField('variable', label=l_('Variables'), rolename='obj',
names=('var', 'ivar', 'cvar')),
GroupedField('exceptions', label=l_('Raises'), rolename='exc',
names=('raises', 'raise', 'exception', 'except'),
can_collapse=True),
Field('returnvalue', label=l_('Returns'), has_arg=False,
names=('returns', 'return')),
Field('returntype', label=l_('Return type'), has_arg=False,
names=('rtype',), rolename='obj'),
]
def get_signature_prefix(self, sig):
"""
May return a prefix to put before the object name in the signature.

257
sphinx/util/docfields.py Normal file
View File

@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
"""
sphinx.util.docfields
~~~~~~~~~~~~~~~~~~~~~
"Doc fields" are reST field lists in object descriptions that will
be domain-specifically transformed to a more appealing presentation.
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from docutils import nodes
from sphinx import addnodes
def _is_only_paragraph(node):
"""True if the node only contains one paragraph (and system messages)."""
if len(node) == 0:
return False
elif len(node) > 1:
for subnode in node[1:]:
if not isinstance(subnode, nodes.system_message):
return False
if isinstance(node[0], nodes.paragraph):
return True
return False
class Field(object):
"""
A doc field that is never grouped. It can have an argument or not, the
argument can be linked using a specified *rolename*. Field should be used
for doc fields that usually don't occur more than once.
Example::
:returns: description of the return value
:rtype: description of the return type
"""
is_grouped = False
is_typed = False
def __init__(self, name, names=(), label=None, has_arg=True, rolename=None):
self.name = name
self.names = names
self.label = label
self.has_arg = has_arg
self.rolename = rolename
def make_xref(self, rolename, domain, target, innernode=nodes.emphasis):
if not rolename:
return innernode(target, target)
refnode = addnodes.pending_xref('', refdomain=domain, refexplicit=False,
reftype=rolename, reftarget=target)
refnode += innernode(target, target)
return refnode
def make_entry(self, fieldarg, content):
return (fieldarg, content)
def make_field(self, types, domain, item):
fieldarg, content = item
fieldname = nodes.field_name('', self.label)
if fieldarg:
fieldname += nodes.Text(' ')
fieldname += self.make_xref(self.rolename, domain,
fieldarg, nodes.Text)
fieldbody = nodes.field_body('', nodes.paragraph('', *content))
return nodes.field('', fieldname, fieldbody)
class GroupedField(Field):
"""
A doc field that is grouped; i.e., all fields of that type will be
transformed into one field with its body being a bulleted list. It always
has an argument. The argument can be linked using the given *rolename*.
GroupedField should be used for doc fields that can occur more than once.
If *can_collapse* is true, this field will revert to a Field if only used
once.
Example::
:raises ErrorClass: description when it is raised
"""
is_grouped = True
list_type = nodes.bullet_list
def __init__(self, name, names=(), label=None, rolename=None,
can_collapse=False):
Field.__init__(self, name, names, label, True, rolename)
self.can_collapse = can_collapse
def make_field(self, types, domain, items):
fieldname = nodes.field_name('', self.label)
listnode = self.list_type()
if len(items) == 1 and self.can_collapse:
return Field.make_field(self, types, domain, items[0])
for fieldarg, content in items:
par = nodes.paragraph()
par += self.make_xref(self.rolename, domain, fieldarg, nodes.strong)
par += nodes.Text(' -- ')
par += content
listnode += nodes.list_item('', par)
fieldbody = nodes.field_body('', listnode)
return nodes.field('', fieldname, fieldbody)
class TypedField(GroupedField):
"""
A doc field that is grouped and has type information for the arguments. It
always has an argument. The argument can be linked using the given
*rolename*, the type using the given *typerolename*.
Two uses are possible: either parameter and type description are given
separately, using a field from *names* and one from *typenames*,
respectively, or both are given using a field from *names*, see the example.
Example::
:param foo: description of parameter foo
:type foo: SomeClass
-- or --
:param SomeClass foo: description of parameter foo
"""
is_typed = True
def __init__(self, name, names=(), typenames=(), label=None,
rolename=None, typerolename=None):
GroupedField.__init__(self, name, names, label, rolename, False)
self.typenames = typenames
self.typerolename = typerolename
def make_field(self, types, domain, items):
fieldname = nodes.field_name('', self.label)
listnode = self.list_type()
for fieldarg, content in items:
par = nodes.paragraph()
par += self.make_xref(self.rolename, domain, fieldarg, nodes.strong)
if fieldarg in types:
typename = types[fieldarg]
if isinstance(typename, list):
typename = u''.join(n.astext() for n in typename)
par += nodes.Text(' (')
par += self.make_xref(self.typerolename, domain, typename)
par += nodes.Text(')')
par += nodes.Text(' -- ')
par += content
listnode += nodes.list_item('', par)
fieldbody = nodes.field_body('', listnode)
return nodes.field('', fieldname, fieldbody)
class DocFieldTransformer(object):
"""
Transforms field lists in "doc field" syntax into better-looking
equivalents, using the field type definitions given on a domain.
"""
def __init__(self, directive):
self.domain = directive.domain
if not hasattr(directive, '_doc_field_type_map'):
directive.__class__._doc_field_type_map = \
self.preprocess_fieldtypes(directive.__class__.doc_field_types)
self.typemap = directive._doc_field_type_map
def preprocess_fieldtypes(self, types):
typemap = {}
for fieldtype in types:
for name in fieldtype.names:
typemap[name] = fieldtype, 0
if fieldtype.is_typed:
for name in fieldtype.typenames:
typemap[name] = fieldtype, 1
return typemap
def transform_all(self, node):
"""Transform all field list children of a node."""
# don't traverse, only handle field lists that are immediate children
for child in node:
if isinstance(child, nodes.field_list):
self.transform(child)
def transform(self, node):
"""Transform a single field list *node*."""
typemap = self.typemap
entries = []
groupindices = {}
types = {}
# step 1: traverse all fields and collect field types and content
for field in node:
fieldname, fieldbody = field
try:
# split into field type and argument
fieldtype, fieldarg = fieldname.astext().split(None, 1)
except ValueError:
# maybe an argument-less field type?
fieldtype, fieldarg = fieldname.astext(), ''
typedesc, is_typefield = typemap.get(fieldtype, (None, None))
if typedesc is None or \
typedesc.has_arg != bool(fieldarg):
# either the field name is unknown, or the argument doesn't
# match the spec; capitalize field name and be done with it
new_fieldname = fieldtype.capitalize() + ' ' + fieldarg
fieldname[0] = nodes.Text(new_fieldname)
entries.append(field)
continue
typename = typedesc.name
if _is_only_paragraph(fieldbody):
content = fieldbody.children[0].children
else:
content = fieldbody.children
if is_typefield:
types.setdefault(typename, {})[fieldarg] = content
continue
# also support syntax like ``:param type name:``
try:
argtype, argname = fieldarg.split(None, 1)
except ValueError:
pass
else:
types.setdefault(typename, {})[argname] = nodes.Text(argtype)
fieldarg = argname
if typedesc.is_grouped:
if typename in groupindices:
group = entries[groupindices[typename]]
else:
groupindices[typename] = len(entries)
group = [typedesc, []]
entries.append(group)
group[1].append(typedesc.make_entry(fieldarg, content))
else:
entries.append([typedesc,
typedesc.make_entry(fieldarg, content)])
# step 2: all entries are collected, construct the new field list
new_list = nodes.field_list()
for entry in entries:
if isinstance(entry, nodes.field):
# pass-through old field
new_list += entry
else:
fieldtype, content = entry
fieldtypes = types.get(fieldtype.name, {})
new_list += fieldtype.make_field(fieldtypes, self.domain, content)
node.replace_self(new_list)