diff --git a/CHANGES b/CHANGES index ffb4c78aa7d..91da9789a12 100644 --- a/CHANGES +++ b/CHANGES @@ -59,6 +59,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 diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 240db309b4e..cfe420f3916 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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 @@ 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 @@ -407,16 +415,7 @@ 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): @@ -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]]): diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index f4b4dd35e57..5102e6706b0 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -23,6 +23,11 @@ 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 diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index ca0148cbb86..b32d7469933 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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) diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index b178889ef13..6a4961ff6c8 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -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