Merge pull request #9155 from jakobandersen/field_roles

Call roles in typed fields
This commit is contained in:
Jakob Lykke Andersen 2021-06-03 19:28:19 +02:00 committed by GitHub
commit 92c5ee07be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 97 additions and 34 deletions

View File

@ -70,6 +70,8 @@ Bugs fixed
* #9280: py domain: "exceptions" module is not displayed
* #9224: ``:param:`` and ``:type:`` fields does not support a type containing
whitespace (ex. ``Dict[str, str]``)
* #8945: when transforming typed fields, call the specified role instead of
making an single xref. For C and C++, use the ``expr`` role for typed fields.
Testing
--------

View File

@ -3115,7 +3115,7 @@ class CObject(ObjectDescription[ASTDeclaration]):
doc_field_types = [
TypedField('parameter', label=_('Parameters'),
names=('param', 'parameter', 'arg', 'argument'),
typerolename='type', typenames=('type',)),
typerolename='expr', typenames=('type',)),
Field('returnvalue', label=_('Returns'), has_arg=False,
names=('returns', 'return')),
Field('returntype', label=_('Return type'), has_arg=False,

View File

@ -6846,7 +6846,7 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
GroupedField('template parameter', label=_('Template Parameters'),
names=('tparam', 'template parameter'),
can_collapse=True),
GroupedField('exceptions', label=_('Throws'), rolename='cpp:class',
GroupedField('exceptions', label=_('Throws'), rolename='expr',
names=('throws', 'throw', 'exception'),
can_collapse=True),
Field('returnvalue', label=_('Returns'), has_arg=False,

View File

@ -215,7 +215,7 @@ class JSCallable(JSObject):
TypedField('arguments', label=_('Arguments'),
names=('argument', 'arg', 'parameter', 'param'),
typerolename='func', typenames=('paramtype', 'type')),
GroupedField('errors', label=_('Throws'), rolename='err',
GroupedField('errors', label=_('Throws'), rolename='func',
names=('throws', ),
can_collapse=True),
Field('returnvalue', label=_('Returns'), has_arg=False,

View File

@ -20,6 +20,7 @@ from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tu
from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst import directives
from docutils.parsers.rst.states import Inliner
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref, pending_xref_condition
@ -284,9 +285,13 @@ def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
class PyXrefMixin:
def make_xref(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = nodes.emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> Node:
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> Node:
# we use inliner=None to make sure we get the old behaviour with a single
# pending_xref node
result = super().make_xref(rolename, domain, target, # type: ignore
innernode, contnode, env)
innernode, contnode,
env, inliner=None, location=None)
result['refspecific'] = True
result['py:module'] = env.ref_context.get('py:module')
result['py:class'] = env.ref_context.get('py:class')
@ -313,7 +318,8 @@ class PyXrefMixin:
def make_xrefs(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = nodes.emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> List[Node]:
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> List[Node]:
delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+|\s*\|\s*|\.\.\.)'
delims_re = re.compile(delims)
sub_targets = re.split(delims, target)
@ -329,7 +335,7 @@ class PyXrefMixin:
results.append(contnode or innernode(sub_target, sub_target))
else:
results.append(self.make_xref(rolename, domain, sub_target,
innernode, contnode, env))
innernode, contnode, env, inliner, location))
return results
@ -337,12 +343,14 @@ class PyXrefMixin:
class PyField(PyXrefMixin, Field):
def make_xref(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = nodes.emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> Node:
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> Node:
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'
return super().make_xref(rolename, domain, target, innernode, contnode, env)
return super().make_xref(rolename, domain, target, innernode, contnode,
env, inliner, location)
class PyGroupedField(PyXrefMixin, GroupedField):
@ -352,12 +360,14 @@ class PyGroupedField(PyXrefMixin, GroupedField):
class PyTypedField(PyXrefMixin, TypedField):
def make_xref(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = nodes.emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> Node:
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> Node:
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'
return super().make_xref(rolename, domain, target, innernode, contnode, env)
return super().make_xref(rolename, domain, target, innernode, contnode,
env, inliner, location)
class PyObject(ObjectDescription[Tuple[str, str]]):

View File

@ -8,19 +8,23 @@
:copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, Union, cast
from docutils import nodes
from docutils.nodes import Node
from docutils.parsers.rst.states import Inliner
from sphinx import addnodes
from sphinx.environment import BuildEnvironment
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.typing import TextlikeNode
if TYPE_CHECKING:
from sphinx.directive import ObjectDescription
logger = logging.getLogger(__name__)
def _is_single_paragraph(node: nodes.field_body) -> bool:
"""True if the node only contains one paragraph (and system messages)."""
@ -62,39 +66,58 @@ class Field:
def make_xref(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> Node:
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> Node:
# note: for backwards compatibility env is last, but not optional
assert env is not None
assert (inliner is None) == (location is None), (inliner, location)
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:
# The domain is passed from DocFieldTransformer. So it surely exists.
# So we don't need to take care the env.get_domain() raises an exception.
role = env.get_domain(domain).role(rolename)
if role is None or inliner is None:
if role is None and inliner is not None:
msg = "Problem in %s domain: field is supposed "
msg += "to use role '%s', but that role is not in the domain."
logger.warning(__(msg), domain, rolename, location=location)
refnode = addnodes.pending_xref('', refdomain=domain, refexplicit=False,
reftype=rolename, reftarget=target)
refnode += contnode or innernode(target, target)
env.get_domain(domain).process_field_xref(refnode)
return refnode
return refnode
lineno = logging.get_source_line(location)[1]
ns, messages = role(rolename, target, target, lineno, inliner, {}, [])
return nodes.inline(target, '', *ns)
def make_xrefs(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> List[Node]:
return [self.make_xref(rolename, domain, target, innernode, contnode, env)]
contnode: Node = None, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> List[Node]:
return [self.make_xref(rolename, domain, target, innernode, contnode,
env, inliner, location)]
def make_entry(self, fieldarg: str, content: List[Node]) -> Tuple[str, List[Node]]:
return (fieldarg, content)
def make_field(self, types: Dict[str, List[Node]], domain: str,
item: Tuple, env: BuildEnvironment = None) -> nodes.field:
item: Tuple, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> 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))
fieldarg, nodes.Text,
env=env, inliner=inliner, location=location))
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)
content[0].astext(), contnode=content[0],
env=env, inliner=inliner, location=location)
fieldbody = nodes.field_body('', nodes.paragraph('', '', *content))
return nodes.field('', fieldname, fieldbody)
@ -121,13 +144,15 @@ class GroupedField(Field):
self.can_collapse = can_collapse
def make_field(self, types: Dict[str, List[Node]], domain: str,
items: Tuple, env: BuildEnvironment = None) -> nodes.field:
items: Tuple, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> 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))
addnodes.literal_strong,
env=env, inliner=inliner, location=location))
par += nodes.Text(' -- ')
par += content
listnode += nodes.list_item('', par)
@ -170,7 +195,8 @@ class TypedField(GroupedField):
self.typerolename = typerolename
def make_field(self, types: Dict[str, List[Node]], domain: str,
items: Tuple, env: BuildEnvironment = None) -> nodes.field:
items: Tuple, env: BuildEnvironment = None,
inliner: Inliner = None, location: Node = None) -> nodes.field:
def handle_item(fieldarg: str, content: str) -> nodes.paragraph:
par = nodes.paragraph()
par.extend(self.make_xrefs(self.rolename, domain, fieldarg,
@ -184,7 +210,8 @@ class TypedField(GroupedField):
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))
addnodes.literal_emphasis, env=env,
inliner=inliner, location=location))
else:
par += fieldtype
par += nodes.Text(')')
@ -227,7 +254,7 @@ class DocFieldTransformer:
"""Transform a single field list *node*."""
typemap = self.typemap
entries: List[Union[nodes.field, Tuple[Field, Any]]] = []
entries: List[Union[nodes.field, Tuple[Field, Any, Node]]] = []
groupindices: Dict[str, int] = {}
types: Dict[str, Dict] = {}
@ -317,16 +344,16 @@ class DocFieldTransformer:
# get one entry per field
if typedesc.is_grouped:
if typename in groupindices:
group = cast(Tuple[Field, List], entries[groupindices[typename]])
group = cast(Tuple[Field, List, Node], entries[groupindices[typename]])
else:
groupindices[typename] = len(entries)
group = (typedesc, [])
group = (typedesc, [], field)
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))
entries.append((typedesc, new_entry, field))
# step 2: all entries are collected, construct the new field list
new_list = nodes.field_list()
@ -335,10 +362,11 @@ class DocFieldTransformer:
# pass-through old field
new_list += entry
else:
fieldtype, items = entry
fieldtype, items, location = 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)
inliner = self.directive.state.inliner
new_list += fieldtype.make_field(fieldtypes, self.directive.domain, items,
env=env, inliner=inliner, location=location)
node.replace_self(new_list)

View File

@ -0,0 +1,4 @@
.. c:function:: void f(int a, int *b)
:param int a:
:param int* b:

View File

@ -0,0 +1,5 @@
.. cpp:function:: void f()
:throws int:
:throws int*:

View File

@ -630,6 +630,13 @@ def test_build_ns_lookup(app, warning):
assert len(ws) == 0
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_build_field_role(app, status, warning):
app.builder.build_all()
ws = filter_warnings(warning, "field-role")
assert len(ws) == 0
def _get_obj(app, queryName):
domain = app.env.get_domain('c')
for name, dispname, objectType, docname, anchor, prio in domain.get_objects():

View File

@ -1237,6 +1237,13 @@ not found in `{test}`
assert any_role.classes == texpr_role.content_classes['a'], expect
@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_build_domain_cpp_field_role(app, status, warning):
app.builder.build_all()
ws = filter_warnings(warning, "field-role")
assert len(ws) == 0
def test_noindexentry(app):
text = (".. cpp:function:: void f()\n"
".. cpp:function:: void g()\n"