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. * 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 the ``viewcode`` extension.
* Added ``html-collect-pages`` event. * 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 Unlike the other pickle files this pickle file requires that the sphinx
module is available on unpickling. 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 # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.addons.*') or your custom ones. # 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', 'sphinx.ext.viewcode'] 'sphinx.ext.autosummary']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']

View File

@ -11,12 +11,11 @@
import re import re
from docutils import nodes
from docutils.parsers.rst import Directive, directives from docutils.parsers.rst import Directive, directives
from docutils.parsers.rst.directives import images from docutils.parsers.rst.directives import images
from sphinx import addnodes from sphinx import addnodes
from sphinx.locale import l_ from sphinx.util.docfields import DocFieldTransformer
# import and register directives # import and register directives
from sphinx.directives.code import * from sphinx.directives.code import *
@ -36,179 +35,6 @@ except AttributeError:
strip_backslash_re = re.compile(r'\\(?=[^\\])') 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): class ObjectDescription(Directive):
""" """
Directive to describe a class, function or similar object. Not used Directive to describe a class, function or similar object. Not used
@ -224,139 +50,8 @@ class ObjectDescription(Directive):
'module': directives.unchanged, 'module': directives.unchanged,
} }
doc_field_types = [ # types of doc fields that this directive handles, see sphinx.util.docfields
TypedFieldType('parameter', doc_field_types = []
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)
def get_signatures(self): def get_signatures(self):
""" """
@ -422,7 +117,7 @@ class ObjectDescription(Directive):
# this is strictly domain-specific (i.e. no assumptions may # this is strictly domain-specific (i.e. no assumptions may
# be made in this base class) # be made in this base class)
name = self.handle_signature(sig, signode) name = self.handle_signature(sig, signode)
except ValueError, err: except ValueError:
# signature parsing failed # signature parsing failed
signode.clear() signode.clear()
signode += addnodes.desc_name(sig, sig) signode += addnodes.desc_name(sig, sig)
@ -463,7 +158,7 @@ class DefaultDomain(Directive):
def run(self): def run(self):
env = self.state.document.settings.env 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) env.doc_read_data['default_domain'] = env.domains.get(domain_name)

View File

@ -13,7 +13,6 @@ import re
import string import string
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import directives
from sphinx import addnodes from sphinx import addnodes
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
@ -21,7 +20,7 @@ from sphinx.locale import l_
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.util import make_refnode 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 # RE to split at word boundaries
@ -48,6 +47,16 @@ class CObject(ObjectDescription):
Description of a C language object. 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 # These C types aren't described anywhere, so don't try to create
# a cross-reference to them # a cross-reference to them
stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct')) 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.directives import ObjectDescription
from sphinx.util import make_refnode from sphinx.util import make_refnode
from sphinx.util.compat import Directive from sphinx.util.compat import Directive
from sphinx.util.docfields import Field, GroupedField, TypedField
# REs for Python signatures # REs for Python signatures
@ -40,6 +41,22 @@ class PyObject(ObjectDescription):
Description of a general Python object. 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): def get_signature_prefix(self, sig):
""" """
May return a prefix to put before the object name in the signature. 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)