From c98236bc61ac24fbc88eb65f99935b41a4b0a3d2 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sun, 17 Jan 2010 12:05:44 +0100 Subject: [PATCH] Finish new doc field handling implementation. --- CHANGES | 3 + doc/builders.rst | 20 --- doc/conf.py | 2 +- sphinx/directives/__init__.py | 315 +--------------------------------- sphinx/domains/c.py | 13 +- sphinx/domains/python.py | 17 ++ sphinx/util/docfields.py | 257 +++++++++++++++++++++++++++ 7 files changed, 294 insertions(+), 333 deletions(-) create mode 100644 sphinx/util/docfields.py diff --git a/CHANGES b/CHANGES index 8b605fa33..0367be1a0 100644 --- a/CHANGES +++ b/CHANGES @@ -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. diff --git a/doc/builders.rst b/doc/builders.rst index 0d1c1351a..fe326c684 100644 --- a/doc/builders.rst +++ b/doc/builders.rst @@ -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 diff --git a/doc/conf.py b/doc/conf.py index 3ef3ff719..afa330681 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -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'] diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 501ced146..134aaf549 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -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) diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index fc5644091..9cb00e74c 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -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')) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 3a1424ba6..fca1d6fa0 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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. diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py new file mode 100644 index 000000000..98cc077cd --- /dev/null +++ b/sphinx/util/docfields.py @@ -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)