Skip to content

Commit eed0730

Browse files
authored
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'
2 parents be84da0 + 1f71f85 commit eed0730

File tree

5 files changed

+86
-68
lines changed

5 files changed

+86
-68
lines changed

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Bugs fixed
6262
* #9944: LaTeX: extra vertical whitespace for some nested declarations
6363
* #9940: LaTeX: Multi-function declaration in Python domain has cramped
6464
vertical spacing in latexpdf output
65+
* #10015: py domain: types under the "typing" module are not hyperlinked defined
66+
at info-field-list
6567
* #9390: texinfo: Do not emit labels inside footnotes
6668
* #9979: Error level messages were displayed as warning messages
6769

sphinx/domains/python.py

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -80,45 +80,53 @@ class ModuleEntry(NamedTuple):
8080
deprecated: bool
8181

8282

83-
def type_to_xref(target: str, env: BuildEnvironment = None, suppress_prefix: bool = False
84-
) -> addnodes.pending_xref:
85-
"""Convert a type string to a cross reference node."""
86-
if target == 'None' or target.startswith('typing.'):
83+
def parse_reftarget(reftarget: str, suppress_prefix: bool = False
84+
) -> Tuple[str, str, str, bool]:
85+
"""Parse a type string and return (reftype, reftarget, title, refspecific flag)"""
86+
refspecific = False
87+
if reftarget.startswith('.'):
88+
reftarget = reftarget[1:]
89+
title = reftarget
90+
refspecific = True
91+
elif reftarget.startswith('~'):
92+
reftarget = reftarget[1:]
93+
title = reftarget.split('.')[-1]
94+
elif suppress_prefix:
95+
title = reftarget.split('.')[-1]
96+
elif reftarget.startswith('typing.'):
97+
title = reftarget[7:]
98+
else:
99+
title = reftarget
100+
101+
if reftarget == 'None' or reftarget.startswith('typing.'):
87102
# typing module provides non-class types. Obj reference is good to refer them.
88103
reftype = 'obj'
89104
else:
90105
reftype = 'class'
91106

107+
return reftype, reftarget, title, refspecific
108+
109+
110+
def type_to_xref(target: str, env: BuildEnvironment = None, suppress_prefix: bool = False
111+
) -> addnodes.pending_xref:
112+
"""Convert a type string to a cross reference node."""
92113
if env:
93114
kwargs = {'py:module': env.ref_context.get('py:module'),
94115
'py:class': env.ref_context.get('py:class')}
95116
else:
96117
kwargs = {}
97118

98-
refspecific = False
99-
if target.startswith('.'):
100-
target = target[1:]
101-
text = target
102-
refspecific = True
103-
elif target.startswith('~'):
104-
target = target[1:]
105-
text = target.split('.')[-1]
106-
elif suppress_prefix:
107-
text = target.split('.')[-1]
108-
elif target.startswith('typing.'):
109-
text = target[7:]
110-
else:
111-
text = target
119+
reftype, target, title, refspecific = parse_reftarget(target, suppress_prefix)
112120

113121
if env.config.python_use_unqualified_type_names:
114122
# Note: It would be better to use qualname to describe the object to support support
115123
# nested classes. But python domain can't access the real python object because this
116124
# module should work not-dynamically.
117-
shortname = text.split('.')[-1]
125+
shortname = title.split('.')[-1]
118126
contnodes: List[Node] = [pending_xref_condition('', shortname, condition='resolved'),
119-
pending_xref_condition('', text, condition='*')]
127+
pending_xref_condition('', title, condition='*')]
120128
else:
121-
contnodes = [nodes.Text(text)]
129+
contnodes = [nodes.Text(title)]
122130

123131
return pending_xref('', *contnodes,
124132
refdomain='py', reftype=reftype, reftarget=target,
@@ -354,27 +362,27 @@ def make_xref(self, rolename: str, domain: str, target: str,
354362
result = super().make_xref(rolename, domain, target, # type: ignore
355363
innernode, contnode,
356364
env, inliner=None, location=None)
357-
result['refspecific'] = True
358-
result['py:module'] = env.ref_context.get('py:module')
359-
result['py:class'] = env.ref_context.get('py:class')
360-
if target.startswith(('.', '~')):
361-
prefix, result['reftarget'] = target[0], target[1:]
362-
if prefix == '.':
363-
text = target[1:]
364-
elif prefix == '~':
365-
text = target.split('.')[-1]
366-
for node in list(result.traverse(nodes.Text)):
367-
node.parent[node.parent.index(node)] = nodes.Text(text)
368-
break
369-
elif isinstance(result, pending_xref) and env.config.python_use_unqualified_type_names:
370-
children = result.children
371-
result.clear()
372-
373-
shortname = target.split('.')[-1]
374-
textnode = innernode('', shortname)
375-
contnodes = [pending_xref_condition('', '', textnode, condition='resolved'),
376-
pending_xref_condition('', '', *children, condition='*')]
377-
result.extend(contnodes)
365+
if isinstance(result, pending_xref):
366+
result['refspecific'] = True
367+
result['py:module'] = env.ref_context.get('py:module')
368+
result['py:class'] = env.ref_context.get('py:class')
369+
370+
reftype, reftarget, reftitle, _ = parse_reftarget(target)
371+
if reftarget != reftitle:
372+
result['reftype'] = reftype
373+
result['reftarget'] = reftarget
374+
375+
result.clear()
376+
result += innernode(reftitle, reftitle)
377+
elif env.config.python_use_unqualified_type_names:
378+
children = result.children
379+
result.clear()
380+
381+
shortname = target.split('.')[-1]
382+
textnode = innernode('', shortname)
383+
contnodes = [pending_xref_condition('', '', textnode, condition='resolved'),
384+
pending_xref_condition('', '', *children, condition='*')]
385+
result.extend(contnodes)
378386

379387
return result
380388

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

408416

409417
class PyField(PyXrefMixin, Field):
410-
def make_xref(self, rolename: str, domain: str, target: str,
411-
innernode: Type[TextlikeNode] = nodes.emphasis,
412-
contnode: Node = None, env: BuildEnvironment = None,
413-
inliner: Inliner = None, location: Node = None) -> Node:
414-
if rolename == 'class' and target == 'None':
415-
# None is not a type, so use obj role instead.
416-
rolename = 'obj'
417-
418-
return super().make_xref(rolename, domain, target, innernode, contnode,
419-
env, inliner, location)
418+
pass
420419

421420

422421
class PyGroupedField(PyXrefMixin, GroupedField):
423422
pass
424423

425424

426425
class PyTypedField(PyXrefMixin, TypedField):
427-
def make_xref(self, rolename: str, domain: str, target: str,
428-
innernode: Type[TextlikeNode] = nodes.emphasis,
429-
contnode: Node = None, env: BuildEnvironment = None,
430-
inliner: Inliner = None, location: Node = None) -> Node:
431-
if rolename == 'class' and target == 'None':
432-
# None is not a type, so use obj role instead.
433-
rolename = 'obj'
434-
435-
return super().make_xref(rolename, domain, target, innernode, contnode,
436-
env, inliner, location)
426+
pass
437427

438428

439429
class PyObject(ObjectDescription[Tuple[str, str]]):

sphinx/ext/autodoc/typehints.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,21 @@
2323
def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
2424
options: Dict, args: str, retann: str) -> None:
2525
"""Record type hints to env object."""
26+
if app.config.autodoc_typehints_format == 'short':
27+
mode = 'smart'
28+
else:
29+
mode = 'fully-qualified'
30+
2631
try:
2732
if callable(obj):
2833
annotations = app.env.temp_data.setdefault('annotations', {})
2934
annotation = annotations.setdefault(name, OrderedDict())
3035
sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases)
3136
for param in sig.parameters.values():
3237
if param.annotation is not param.empty:
33-
annotation[param.name] = typing.stringify(param.annotation)
38+
annotation[param.name] = typing.stringify(param.annotation, mode)
3439
if sig.return_annotation is not sig.empty:
35-
annotation['return'] = typing.stringify(sig.return_annotation)
40+
annotation['return'] = typing.stringify(sig.return_annotation, mode)
3641
except (TypeError, ValueError):
3742
pass
3843

tests/test_domain_py.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,9 @@ def test_type_field(app):
11961196
text = (".. py:data:: var1\n"
11971197
" :type: .int\n"
11981198
".. py:data:: var2\n"
1199-
" :type: ~builtins.int\n")
1199+
" :type: ~builtins.int\n"
1200+
".. py:data:: var3\n"
1201+
" :type: typing.Optional[typing.Tuple[int, typing.Any]]\n")
12001202
doctree = restructuredtext.parse(app, text)
12011203
assert_node(doctree, (addnodes.index,
12021204
[desc, ([desc_signature, ([desc_name, "var1"],
@@ -1209,9 +1211,28 @@ def test_type_field(app):
12091211
[desc_annotation, ([desc_sig_punctuation, ':'],
12101212
desc_sig_space,
12111213
[pending_xref, "int"])])],
1214+
[desc_content, ()])],
1215+
addnodes.index,
1216+
[desc, ([desc_signature, ([desc_name, "var3"],
1217+
[desc_annotation, ([desc_sig_punctuation, ":"],
1218+
desc_sig_space,
1219+
[pending_xref, "Optional"],
1220+
[desc_sig_punctuation, "["],
1221+
[pending_xref, "Tuple"],
1222+
[desc_sig_punctuation, "["],
1223+
[pending_xref, "int"],
1224+
[desc_sig_punctuation, ","],
1225+
desc_sig_space,
1226+
[pending_xref, "Any"],
1227+
[desc_sig_punctuation, "]"],
1228+
[desc_sig_punctuation, "]"])])],
12121229
[desc_content, ()])]))
12131230
assert_node(doctree[1][0][1][2], pending_xref, reftarget='int', refspecific=True)
12141231
assert_node(doctree[3][0][1][2], pending_xref, reftarget='builtins.int', refspecific=False)
1232+
assert_node(doctree[5][0][1][2], pending_xref, reftarget='typing.Optional', refspecific=False)
1233+
assert_node(doctree[5][0][1][4], pending_xref, reftarget='typing.Tuple', refspecific=False)
1234+
assert_node(doctree[5][0][1][6], pending_xref, reftarget='int', refspecific=False)
1235+
assert_node(doctree[5][0][1][9], pending_xref, reftarget='typing.Any', refspecific=False)
12151236

12161237

12171238
@pytest.mark.sphinx(freshenv=True)

tests/test_ext_autodoc_configs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ def test_autodoc_typehints_description(app):
835835
' **x** (*Tuple**[**int**, **Union**[**int**, **str**]**]*) --\n'
836836
'\n'
837837
' Return type:\n'
838-
' Tuple[int, int]\n'
838+
' *Tuple*[int, int]\n'
839839
in context)
840840

841841
# Overloads still get displayed in the signature
@@ -887,7 +887,7 @@ def test_autodoc_typehints_description_no_undoc(app):
887887
' another tuple\n'
888888
'\n'
889889
' Return type:\n'
890-
' Tuple[int, int]\n'
890+
' *Tuple*[int, int]\n'
891891
in context)
892892

893893

@@ -978,7 +978,7 @@ def test_autodoc_typehints_both(app):
978978
' **x** (*Tuple**[**int**, **Union**[**int**, **str**]**]*) --\n'
979979
'\n'
980980
' Return type:\n'
981-
' Tuple[int, int]\n'
981+
' *Tuple*[int, int]\n'
982982
in context)
983983

984984
# Overloads still get displayed in the signature

0 commit comments

Comments
 (0)