Merge pull request #10021 from tk0miya/10015_typehints_format_with_typehints_in_description

Fix #10015: autodoc: autodoc_typehints_format='short' does not work when autodoc_typehints='description'
This commit is contained in:
Takeshi KOMIYA
2021-12-29 02:09:35 +09:00
committed by GitHub
5 changed files with 85 additions and 67 deletions

View File

@@ -62,6 +62,8 @@ Bugs fixed
* #9944: LaTeX: extra vertical whitespace for some nested declarations
* #9940: LaTeX: Multi-function declaration in Python domain has cramped
vertical spacing in latexpdf output
* #10015: py domain: types under the "typing" module are not hyperlinked defined
at info-field-list
* #9390: texinfo: Do not emit labels inside footnotes
* #9979: Error level messages were displayed as warning messages

View File

@@ -80,45 +80,53 @@ class ModuleEntry(NamedTuple):
deprecated: bool
def type_to_xref(target: str, env: BuildEnvironment = None, suppress_prefix: bool = False
) -> addnodes.pending_xref:
"""Convert a type string to a cross reference node."""
if target == 'None' or target.startswith('typing.'):
def parse_reftarget(reftarget: str, suppress_prefix: bool = False
) -> Tuple[str, str, str, bool]:
"""Parse a type string and return (reftype, reftarget, title, refspecific flag)"""
refspecific = False
if reftarget.startswith('.'):
reftarget = reftarget[1:]
title = reftarget
refspecific = True
elif reftarget.startswith('~'):
reftarget = reftarget[1:]
title = reftarget.split('.')[-1]
elif suppress_prefix:
title = reftarget.split('.')[-1]
elif reftarget.startswith('typing.'):
title = reftarget[7:]
else:
title = reftarget
if reftarget == 'None' or reftarget.startswith('typing.'):
# typing module provides non-class types. Obj reference is good to refer them.
reftype = 'obj'
else:
reftype = 'class'
return reftype, reftarget, title, refspecific
def type_to_xref(target: str, env: BuildEnvironment = None, suppress_prefix: bool = False
) -> addnodes.pending_xref:
"""Convert a type string to a cross reference node."""
if env:
kwargs = {'py:module': env.ref_context.get('py:module'),
'py:class': env.ref_context.get('py:class')}
else:
kwargs = {}
refspecific = False
if target.startswith('.'):
target = target[1:]
text = target
refspecific = True
elif target.startswith('~'):
target = target[1:]
text = target.split('.')[-1]
elif suppress_prefix:
text = target.split('.')[-1]
elif target.startswith('typing.'):
text = target[7:]
else:
text = target
reftype, target, title, refspecific = parse_reftarget(target, suppress_prefix)
if env.config.python_use_unqualified_type_names:
# Note: It would be better to use qualname to describe the object to support support
# nested classes. But python domain can't access the real python object because this
# module should work not-dynamically.
shortname = text.split('.')[-1]
shortname = title.split('.')[-1]
contnodes: List[Node] = [pending_xref_condition('', shortname, condition='resolved'),
pending_xref_condition('', text, condition='*')]
pending_xref_condition('', title, condition='*')]
else:
contnodes = [nodes.Text(text)]
contnodes = [nodes.Text(title)]
return pending_xref('', *contnodes,
refdomain='py', reftype=reftype, reftarget=target,
@@ -354,27 +362,27 @@ class PyXrefMixin:
result = super().make_xref(rolename, domain, target, # type: ignore
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')
if target.startswith(('.', '~')):
prefix, result['reftarget'] = target[0], target[1:]
if prefix == '.':
text = target[1:]
elif prefix == '~':
text = target.split('.')[-1]
for node in list(result.traverse(nodes.Text)):
node.parent[node.parent.index(node)] = nodes.Text(text)
break
elif isinstance(result, pending_xref) and env.config.python_use_unqualified_type_names:
children = result.children
result.clear()
if isinstance(result, pending_xref):
result['refspecific'] = True
result['py:module'] = env.ref_context.get('py:module')
result['py:class'] = env.ref_context.get('py:class')
shortname = target.split('.')[-1]
textnode = innernode('', shortname)
contnodes = [pending_xref_condition('', '', textnode, condition='resolved'),
pending_xref_condition('', '', *children, condition='*')]
result.extend(contnodes)
reftype, reftarget, reftitle, _ = parse_reftarget(target)
if reftarget != reftitle:
result['reftype'] = reftype
result['reftarget'] = reftarget
result.clear()
result += innernode(reftitle, reftitle)
elif env.config.python_use_unqualified_type_names:
children = result.children
result.clear()
shortname = target.split('.')[-1]
textnode = innernode('', shortname)
contnodes = [pending_xref_condition('', '', textnode, condition='resolved'),
pending_xref_condition('', '', *children, condition='*')]
result.extend(contnodes)
return result
@@ -407,16 +415,7 @@ 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,
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, inliner, location)
pass
class PyGroupedField(PyXrefMixin, GroupedField):
@@ -424,16 +423,7 @@ 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,
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, inliner, location)
pass
class PyObject(ObjectDescription[Tuple[str, str]]):

View File

@@ -23,6 +23,11 @@ from sphinx.util import inspect, typing
def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
options: Dict, args: str, retann: str) -> None:
"""Record type hints to env object."""
if app.config.autodoc_typehints_format == 'short':
mode = 'smart'
else:
mode = 'fully-qualified'
try:
if callable(obj):
annotations = app.env.temp_data.setdefault('annotations', {})
@@ -30,9 +35,9 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases)
for param in sig.parameters.values():
if param.annotation is not param.empty:
annotation[param.name] = typing.stringify(param.annotation)
annotation[param.name] = typing.stringify(param.annotation, mode)
if sig.return_annotation is not sig.empty:
annotation['return'] = typing.stringify(sig.return_annotation)
annotation['return'] = typing.stringify(sig.return_annotation, mode)
except (TypeError, ValueError):
pass

View File

@@ -1196,7 +1196,9 @@ def test_type_field(app):
text = (".. py:data:: var1\n"
" :type: .int\n"
".. py:data:: var2\n"
" :type: ~builtins.int\n")
" :type: ~builtins.int\n"
".. py:data:: var3\n"
" :type: typing.Optional[typing.Tuple[int, typing.Any]]\n")
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, ([desc_name, "var1"],
@@ -1209,9 +1211,28 @@ def test_type_field(app):
[desc_annotation, ([desc_sig_punctuation, ':'],
desc_sig_space,
[pending_xref, "int"])])],
[desc_content, ()])],
addnodes.index,
[desc, ([desc_signature, ([desc_name, "var3"],
[desc_annotation, ([desc_sig_punctuation, ":"],
desc_sig_space,
[pending_xref, "Optional"],
[desc_sig_punctuation, "["],
[pending_xref, "Tuple"],
[desc_sig_punctuation, "["],
[pending_xref, "int"],
[desc_sig_punctuation, ","],
desc_sig_space,
[pending_xref, "Any"],
[desc_sig_punctuation, "]"],
[desc_sig_punctuation, "]"])])],
[desc_content, ()])]))
assert_node(doctree[1][0][1][2], pending_xref, reftarget='int', refspecific=True)
assert_node(doctree[3][0][1][2], pending_xref, reftarget='builtins.int', refspecific=False)
assert_node(doctree[5][0][1][2], pending_xref, reftarget='typing.Optional', refspecific=False)
assert_node(doctree[5][0][1][4], pending_xref, reftarget='typing.Tuple', refspecific=False)
assert_node(doctree[5][0][1][6], pending_xref, reftarget='int', refspecific=False)
assert_node(doctree[5][0][1][9], pending_xref, reftarget='typing.Any', refspecific=False)
@pytest.mark.sphinx(freshenv=True)

View File

@@ -835,7 +835,7 @@ def test_autodoc_typehints_description(app):
' **x** (*Tuple**[**int**, **Union**[**int**, **str**]**]*) --\n'
'\n'
' Return type:\n'
' Tuple[int, int]\n'
' *Tuple*[int, int]\n'
in context)
# Overloads still get displayed in the signature
@@ -887,7 +887,7 @@ def test_autodoc_typehints_description_no_undoc(app):
' another tuple\n'
'\n'
' Return type:\n'
' Tuple[int, int]\n'
' *Tuple*[int, int]\n'
in context)
@@ -978,7 +978,7 @@ def test_autodoc_typehints_both(app):
' **x** (*Tuple**[**int**, **Union**[**int**, **str**]**]*) --\n'
'\n'
' Return type:\n'
' Tuple[int, int]\n'
' *Tuple*[int, int]\n'
in context)
# Overloads still get displayed in the signature