Skip to content

Commit

Permalink
Merge pull request #10021 from tk0miya/10015_typehints_format_with_ty…
Browse files Browse the repository at this point in the history
…pehints_in_description

Fix #10015: autodoc: autodoc_typehints_format='short' does not work when autodoc_typehints='description'
  • Loading branch information
tk0miya committed Dec 28, 2021
2 parents be84da0 + 1f71f85 commit eed0730
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 68 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -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

Expand Down
114 changes: 52 additions & 62 deletions sphinx/domains/python.py
Expand Up @@ -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,
Expand Down Expand Up @@ -354,27 +362,27 @@ def make_xref(self, rolename: str, domain: str, target: str,
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()

shortname = target.split('.')[-1]
textnode = innernode('', shortname)
contnodes = [pending_xref_condition('', '', textnode, condition='resolved'),
pending_xref_condition('', '', *children, condition='*')]
result.extend(contnodes)
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')

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

Expand Down Expand Up @@ -407,33 +415,15 @@ def make_xrefs(self, rolename: str, domain: str, target: str,


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):
pass


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]]):
Expand Down
9 changes: 7 additions & 2 deletions sphinx/ext/autodoc/typehints.py
Expand Up @@ -23,16 +23,21 @@
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', {})
annotation = annotations.setdefault(name, OrderedDict())
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

Expand Down
23 changes: 22 additions & 1 deletion tests/test_domain_py.py
Expand Up @@ -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"],
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_ext_autodoc_configs.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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
Expand Down

0 comments on commit eed0730

Please sign in to comment.