# -*- 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-2011 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ from docutils import nodes from sphinx import addnodes def _is_single_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, can_collapse=False): GroupedField.__init__(self, name, names, label, rolename, can_collapse) self.typenames = typenames self.typerolename = typerolename def make_field(self, types, domain, items): def handle_item(fieldarg, content): par = nodes.paragraph() par += self.make_xref(self.rolename, domain, fieldarg, nodes.strong) if fieldarg in types: par += nodes.Text(' (') # NOTE: using .pop() here to prevent a single type node to be # inserted twice into the doctree, which leads to # inconsistencies later when references are resolved fieldtype = types.pop(fieldarg) if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text): typename = u''.join(n.astext() for n in fieldtype) par += self.make_xref(self.typerolename, domain, typename) else: par += fieldtype par += nodes.Text(')') par += nodes.Text(' -- ') par += content return par fieldname = nodes.field_name('', self.label) if len(items) == 1 and self.can_collapse: fieldarg, content = items[0] bodynode = handle_item(fieldarg, content) else: bodynode = self.list_type() for fieldarg, content in items: bodynode += nodes.list_item('', handle_item(fieldarg, content)) fieldbody = nodes.field_body('', bodynode) 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 '_doc_field_type_map' not in directive.__class__.__dict__: 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, False if fieldtype.is_typed: for name in fieldtype.typenames: typemap[name] = fieldtype, True 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)) # sort out unknown fields 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 # collect the content, trying not to keep unnecessary paragraphs if _is_single_paragraph(fieldbody): content = fieldbody.children[0].children else: content = fieldbody.children # if the field specifies a type, put it in the types collection if is_typefield: # filter out only inline nodes; others will result in invalid # markup being written out content = filter( lambda n: isinstance(n, nodes.Inline) or isinstance(n, nodes.Text), content) if content: types.setdefault(typename, {})[fieldarg] = content continue # also support syntax like ``:param type name:`` if typedesc.is_typed: try: argtype, argname = fieldarg.split(None, 1) except ValueError: pass else: types.setdefault(typename, {})[argname] = \ [nodes.Text(argtype)] fieldarg = argname # grouped entries need to be collected in one entry, while others # get one entry per field 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)