mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #9155 from jakobandersen/field_roles
Call roles in typed fields
This commit is contained in:
commit
92c5ee07be
2
CHANGES
2
CHANGES
@ -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
|
||||
--------
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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]]):
|
||||
|
@ -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)
|
||||
|
4
tests/roots/test-domain-c/field-role.rst
Normal file
4
tests/roots/test-domain-c/field-role.rst
Normal file
@ -0,0 +1,4 @@
|
||||
.. c:function:: void f(int a, int *b)
|
||||
|
||||
:param int a:
|
||||
:param int* b:
|
5
tests/roots/test-domain-cpp/field-role.rst
Normal file
5
tests/roots/test-domain-cpp/field-role.rst
Normal file
@ -0,0 +1,5 @@
|
||||
.. cpp:function:: void f()
|
||||
|
||||
:throws int:
|
||||
:throws int*:
|
||||
|
@ -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():
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user