Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
395 lines (338 sloc) 15.9 KB
"""
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-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import warnings
from typing import List, Tuple, cast
from docutils import nodes
from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx40Warning
if False:
# For type annotation
from typing import Any, Dict, Type, Union # NOQA
from sphinx.directive import ObjectDescription # NOQA
from sphinx.environment import BuildEnvironment # NOQA
from sphinx.util.typing import TextlikeNode # NOQA
def _is_single_paragraph(node):
# type: (nodes.field_body) -> bool
"""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:]: # type: nodes.Node
if not isinstance(subnode, nodes.system_message):
return False
if isinstance(node[0], nodes.paragraph):
return True
return False
class Field:
"""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.
The body can be linked using a specified *bodyrolename* if the content is
just a single inline or text node.
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,
bodyrolename=None):
# type: (str, Tuple[str, ...], str, bool, str, str) -> None
self.name = name
self.names = names
self.label = label
self.has_arg = has_arg
self.rolename = rolename
self.bodyrolename = bodyrolename
def make_xref(self,
rolename, # type: str
domain, # type: str
target, # type: str
innernode=addnodes.literal_emphasis, # type: Type[TextlikeNode]
contnode=None, # type: nodes.Node
env=None, # type: BuildEnvironment
):
# type: (...) -> nodes.Node
if not rolename:
return contnode or innernode(target, target)
refnode = addnodes.pending_xref('', refdomain=domain, refexplicit=False,
reftype=rolename, reftarget=target)
refnode += contnode or innernode(target, target)
if env:
env.get_domain(domain).process_field_xref(refnode)
return refnode
def make_xrefs(self,
rolename, # type: str
domain, # type: str
target, # type: str
innernode=addnodes.literal_emphasis, # type: Type[TextlikeNode]
contnode=None, # type: nodes.Node
env=None, # type: BuildEnvironment
):
# type: (...) -> List[nodes.Node]
return [self.make_xref(rolename, domain, target, innernode, contnode, env)]
def make_entry(self, fieldarg, content):
# type: (str, List[nodes.Node]) -> Tuple[str, List[nodes.Node]]
return (fieldarg, content)
def make_field(self,
types, # type: Dict[str, List[nodes.Node]]
domain, # type: str
item, # type: Tuple
env=None, # type: BuildEnvironment
):
# type: (...) -> nodes.field
fieldarg, content = item
fieldname = nodes.field_name('', self.label)
if fieldarg:
fieldname += nodes.Text(' ')
fieldname.extend(self.make_xrefs(self.rolename, domain,
fieldarg, nodes.Text, env=env))
if len(content) == 1 and (
isinstance(content[0], nodes.Text) or
(isinstance(content[0], nodes.inline) and len(content[0]) == 1 and
isinstance(content[0][0], nodes.Text))):
content = self.make_xrefs(self.bodyrolename, domain,
content[0].astext(), contnode=content[0], env=env)
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):
# type: (str, Tuple[str, ...], str, str, bool) -> None
super().__init__(name, names, label, True, rolename)
self.can_collapse = can_collapse
def make_field(self,
types, # type: Dict[str, List[nodes.Node]]
domain, # type: str
items, # type: Tuple
env=None, # type: BuildEnvironment
):
# type: (...) -> nodes.field
fieldname = nodes.field_name('', self.label)
listnode = self.list_type()
for fieldarg, content in items:
par = nodes.paragraph()
par.extend(self.make_xrefs(self.rolename, domain, fieldarg,
addnodes.literal_strong, env=env))
par += nodes.Text(' -- ')
par += content
listnode += nodes.list_item('', par)
if len(items) == 1 and self.can_collapse:
list_item = cast(nodes.list_item, listnode[0])
fieldbody = nodes.field_body('', list_item[0])
return nodes.field('', fieldname, fieldbody)
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):
# type: (str, Tuple[str, ...], Tuple[str, ...], str, str, str, bool) -> None
super().__init__(name, names, label, rolename, can_collapse)
self.typenames = typenames
self.typerolename = typerolename
def make_field(self,
types, # type: Dict[str, List[nodes.Node]]
domain, # type: str
items, # type: Tuple
env=None, # type: BuildEnvironment
):
# type: (...) -> nodes.field
def handle_item(fieldarg, content):
# type: (str, str) -> nodes.paragraph
par = nodes.paragraph()
par.extend(self.make_xrefs(self.rolename, domain, fieldarg,
addnodes.literal_strong, env=env))
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 = fieldtype[0].astext()
par.extend(self.make_xrefs(self.typerolename, domain, typename,
addnodes.literal_emphasis, env=env))
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) # type: nodes.Node
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:
"""
Transforms field lists in "doc field" syntax into better-looking
equivalents, using the field type definitions given on a domain.
"""
typemap = None # type: Dict[str, Tuple[Field, bool]]
def __init__(self, directive):
# type: (ObjectDescription) -> None
self.directive = directive
self.typemap = directive.get_field_type_map()
def preprocess_fieldtypes(self, types):
# type: (List[Field]) -> Dict[str, Tuple[Field, bool]]
warnings.warn('DocFieldTransformer.preprocess_fieldtypes() is deprecated.',
RemovedInSphinx40Warning)
typemap = {}
for fieldtype in types:
for name in fieldtype.names:
typemap[name] = fieldtype, False
if fieldtype.is_typed:
typed_field = cast(TypedField, fieldtype)
for name in typed_field.typenames:
typemap[name] = typed_field, True
return typemap
def transform_all(self, node):
# type: (addnodes.desc_content) -> None
"""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):
# type: (nodes.field_list) -> None
"""Transform a single field list *node*."""
typemap = self.typemap
entries = [] # type: List[Union[nodes.field, Tuple[Field, Any]]]
groupindices = {} # type: Dict[str, int]
types = {} # type: Dict[str, Dict]
# step 1: traverse all fields and collect field types and content
for field in cast(List[nodes.field], node):
assert len(field) == 2
field_name = cast(nodes.field_name, field[0])
field_body = cast(nodes.field_body, field[1])
try:
# split into field type and argument
fieldtype_name, fieldarg = field_name.astext().split(None, 1)
except ValueError:
# maybe an argument-less field type?
fieldtype_name, fieldarg = field_name.astext(), ''
typedesc, is_typefield = typemap.get(fieldtype_name, (None, None))
# collect the content, trying not to keep unnecessary paragraphs
if _is_single_paragraph(field_body):
paragraph = cast(nodes.paragraph, field_body[0])
content = paragraph.children
else:
content = field_body.children
# 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_name[0:1].upper() + fieldtype_name[1:]
if fieldarg:
new_fieldname += ' ' + fieldarg
field_name[0] = nodes.Text(new_fieldname)
entries.append(field)
# but if this has a type then we can at least link it
if (typedesc and is_typefield and content and
len(content) == 1 and isinstance(content[0], nodes.Text)):
typed_field = cast(TypedField, typedesc)
target = content[0].astext()
xrefs = typed_field.make_xrefs(
typed_field.typerolename,
self.directive.domain,
target,
contnode=content[0],
)
if _is_single_paragraph(field_body):
paragraph = cast(nodes.paragraph, field_body[0])
paragraph.clear()
paragraph.extend(xrefs)
else:
field_body.clear()
field_body += nodes.paragraph('', '', *xrefs)
continue
typename = typedesc.name
# 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 = [n for n in content if isinstance(n, nodes.Inline) or
isinstance(n, nodes.Text)]
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
translatable_content = nodes.inline(field_body.rawsource,
translatable=True)
translatable_content.document = field_body.parent.document
translatable_content.source = field_body.parent.source
translatable_content.line = field_body.parent.line
translatable_content += content
# 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 = cast(Tuple[Field, List], entries[groupindices[typename]])
else:
groupindices[typename] = len(entries)
group = (typedesc, [])
entries.append(group)
new_entry = typedesc.make_entry(fieldarg, [translatable_content])
group[1].append(new_entry)
else:
new_entry = typedesc.make_entry(fieldarg, [translatable_content])
entries.append((typedesc, new_entry))
# 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, items = entry
fieldtypes = types.get(fieldtype.name, {})
env = self.directive.state.document.settings.env
new_list += fieldtype.make_field(fieldtypes, self.directive.domain,
items, env=env)
node.replace_self(new_list)
You can’t perform that action at this time.