diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 79d7e4f466d..8df78f7085a 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -1233,6 +1233,10 @@ def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: if mod.docname in docnames: self.modules[modname] = mod + def process_field_xref(self, pnode: pending_xref) -> None: + pnode['py:module'] = self.env.ref_context.get('py:module') + pnode['py:class'] = self.env.ref_context.get('py:class') + def find_obj(self, env: BuildEnvironment, modname: str, classname: str, name: str, type: str, searchmode: int = 0 ) -> List[Tuple[str, ObjectEntry]]: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 2dc97bed99e..a4aa58cf343 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -11,6 +11,8 @@ import sys from unittest.mock import Mock +from copy import deepcopy + import pytest from docutils import nodes @@ -20,12 +22,25 @@ desc_sig_name, desc_sig_operator, desc_sig_punctuation, desc_signature, pending_xref) from sphinx.domains import IndexEntry -from sphinx.domains.python import (PythonDomain, PythonModuleIndex, _parse_annotation, - _pseudo_parse_arglist, py_sig_re) +from sphinx.domains.python import (PyField, PyTypedField, PythonDomain, PythonModuleIndex, + _parse_annotation, _pseudo_parse_arglist, py_sig_re) from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +class DummyEnv: + def __init__(self) -> None: + self.domaindata = {} + self.ref_context = {} + self.domains = {} + self.docname = 'doc' + self.temp_data = {} + self.config = Mock() + + def get_domain(self, name: str): + return self.domains[name] + + def parse(sig): m = py_sig_re.match(sig) if m is None: @@ -113,11 +128,11 @@ def assert_refnode(node, module_name, class_name, target, reftype=None, 'ModTopLevel', 'class') assert_refnode(refnodes[8], 'module_b.submodule', 'ModTopLevel', 'ModNoModule', 'class') - assert_refnode(refnodes[9], False, False, 'int', 'class') - assert_refnode(refnodes[10], False, False, 'tuple', 'class') - assert_refnode(refnodes[11], False, False, 'str', 'class') - assert_refnode(refnodes[12], False, False, 'float', 'class') - assert_refnode(refnodes[13], False, False, 'list', 'class') + assert_refnode(refnodes[9], 'module_b.submodule', None, 'int', 'class') + assert_refnode(refnodes[10], 'module_b.submodule', None, 'tuple', 'class') + assert_refnode(refnodes[11], 'module_b.submodule', None, 'str', 'class') + assert_refnode(refnodes[12], 'module_b.submodule', None, 'float', 'class') + assert_refnode(refnodes[13], 'module_b.submodule', None, 'list', 'class') assert_refnode(refnodes[14], False, False, 'ModTopLevel', 'class') assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std') assert len(refnodes) == 16 @@ -206,6 +221,61 @@ def find_obj(modname, prefix, obj_name, obj_type, searchmode=0): ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method'))]) +def test_docfield_pending_xref_includes_scope(): + env = DummyEnv() + domain = PythonDomain(env) + env.domains['py'] = domain + + env.ref_context['py:module'] = 'mod.submod' + env.ref_context['py:class'] = 'Outer' + + typed_field = PyTypedField('parameter', names=('param',), typenames=('type',), + label='Parameters', typerolename='class') + type_xref = typed_field.make_xref('class', 'py', 'A', env=env) + assert isinstance(type_xref, pending_xref) + assert type_xref['py:module'] == 'mod.submod' + assert type_xref['py:class'] == 'Outer' + + return_field = PyField('returntype', names=('rtype',), label='Return type', + has_arg=False, bodyrolename='class') + return_xref = return_field.make_xref('class', 'py', 'A', env=env) + assert isinstance(return_xref, pending_xref) + assert return_xref['py:module'] == 'mod.submod' + assert return_xref['py:class'] == 'Outer' + + +def test_docfield_xref_resolution_prefers_current_scope(): + env = DummyEnv() + domain = PythonDomain(env) + env.domains['py'] = domain + + env.docname = 'mod_doc' + domain.note_object('mod.A', 'class', 'mod.A') + env.docname = 'sub_doc' + domain.note_object('mod.submod.A', 'class', 'mod.submod.A') + + env.ref_context['py:module'] = 'mod.submod' + env.ref_context['py:class'] = 'Outer' + + field = PyField('returntype', names=('rtype',), label='Return type', has_arg=False, + bodyrolename='class') + xref = field.make_xref('class', 'py', 'A', env=env) + + ambiguous = domain.find_obj(env, None, None, 'A', 'class', searchmode=1) + assert {name for name, _ in ambiguous} == {'mod.A', 'mod.submod.A'} + + scoped = domain.find_obj(env, xref.get('py:module'), xref.get('py:class'), + xref['reftarget'], 'class', searchmode=1) + assert scoped == [('mod.submod.A', domain.objects['mod.submod.A'])] + + builder = Mock() + contnode = deepcopy(xref[0]) + result = domain.resolve_xref(env, 'sub_doc', builder, 'class', xref['reftarget'], xref, + contnode) + assert isinstance(result, nodes.reference) + assert result['refid'] == 'mod.submod.A' + + def test_get_full_qualified_name(): env = Mock(domaindata={}) domain = PythonDomain(env)