From 9eb5097a561825bcba8639d5f9fb69bb37685447 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sun, 9 Feb 2025 21:38:51 +0000 Subject: [PATCH] Refactor ``ReferencesResolver`` (#13321) Split ``ReferencesResolver.run()`` into smaller parts. --- sphinx/ext/intersphinx/_load.py | 3 +- sphinx/ext/intersphinx/_resolve.py | 14 +- sphinx/transforms/post_transforms/__init__.py | 204 ++++++++++-------- 3 files changed, 126 insertions(+), 95 deletions(-) diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index 4a14c8b18..6a07dbbc3 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -30,7 +30,8 @@ if TYPE_CHECKING: InventoryName, InventoryURI, ) - from sphinx.util.typing import Inventory, _Inventory + from sphinx.util.inventory import _Inventory + from sphinx.util.typing import Inventory def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None: diff --git a/sphinx/ext/intersphinx/_resolve.py b/sphinx/ext/intersphinx/_resolve.py index 4e819f4b7..e52a53898 100644 --- a/sphinx/ext/intersphinx/_resolve.py +++ b/sphinx/ext/intersphinx/_resolve.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from sphinx.environment import BuildEnvironment from sphinx.ext.intersphinx._shared import InventoryName from sphinx.util.inventory import _InventoryItem - from sphinx.util.typing import RoleFunction, _Inventory + from sphinx.util.typing import Inventory, RoleFunction def _create_element_from_result( @@ -75,7 +75,7 @@ def _create_element_from_result( def _resolve_reference_in_domain_by_target( inv_name: InventoryName | None, - inventory: _Inventory, + inventory: Inventory, domain_name: str, objtypes: Iterable[str], target: str, @@ -139,7 +139,7 @@ def _resolve_reference_in_domain_by_target( def _resolve_reference_in_domain( inv_name: InventoryName | None, - inventory: _Inventory, + inventory: Inventory, honor_disabled_refs: bool, disabled_reftypes: Set[str], domain: Domain, @@ -190,7 +190,7 @@ def _resolve_reference_in_domain( def _resolve_reference( inv_name: InventoryName | None, domains: _DomainsContainer, - inventory: _Inventory, + inventory: Inventory, honor_disabled_refs: bool, disabled_reftypes: Set[str], node: pending_xref, @@ -305,7 +305,7 @@ def resolve_reference_detect_inventory( Resolution is tried first with the target as is in any inventory. If this does not succeed, then the target is split by the first ``:``, - to form ``inv_name:newtarget``. If ``inv_name`` is a named inventory, then resolution + to form ``inv_name:new_target``. If ``inv_name`` is a named inventory, then resolution is tried in that inventory with the new target. """ # ordinary direct lookup, use data as is @@ -317,10 +317,10 @@ def resolve_reference_detect_inventory( target = node['reftarget'] if ':' not in target: return None - inv_name, newtarget = target.split(':', 1) + inv_name, _, new_target = target.partition(':') if not inventory_exists(env, inv_name): return None - node['reftarget'] = newtarget + node['reftarget'] = new_target res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode) node['reftarget'] = target return res_inv diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 8c92fdb83..fa108bf34 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -72,101 +72,137 @@ class ReferencesResolver(SphinxPostTransform): else: contnode = cast('Element', node[0].deepcopy()) - newnode = None - - typ = node['reftype'] - target = node['reftarget'] - node.setdefault('refdoc', self.env.docname) - refdoc = node.get('refdoc') - domain = None - - try: - if node.get('refdomain', False): - # let the domain try to resolve the reference - try: - domain = self.env.domains[node['refdomain']] - except KeyError as exc: - raise NoUri(target, typ) from exc - newnode = domain.resolve_xref( - self.env, refdoc, self.app.builder, typ, target, node, contnode - ) - # really hardwired reference types - elif typ == 'any': - newnode = self.resolve_anyref(refdoc, node, contnode) - # no new node found? try the missing-reference event - if newnode is None: - newnode = self.app.emit_firstresult( - 'missing-reference', - self.env, - node, - contnode, - allowed_exceptions=(NoUri,), - ) - # still not found? warn if node wishes to be warned about or - # we are in nitpicky mode - if newnode is None: - self.warn_missing_reference(refdoc, typ, target, node, domain) - except NoUri: - newnode = None - - if newnode: - newnodes: list[Node] = [newnode] + new_node = self._resolve_pending_xref(node, contnode) + if new_node: + new_nodes: list[Node] = [new_node] else: - newnodes = [contnode] - if newnode is None and isinstance( + new_nodes = [contnode] + if new_node is None and isinstance( node[0], addnodes.pending_xref_condition ): matched = self.find_pending_xref_condition(node, ('*',)) if matched: - newnodes = matched + new_nodes = matched else: - logger.warning( - __( - 'Could not determine the fallback text for the ' - 'cross-reference. Might be a bug.' - ), - location=node, + msg = __( + 'Could not determine the fallback text for the ' + 'cross-reference. Might be a bug.' ) + logger.warning(msg, location=node) - node.replace_self(newnodes) + node.replace_self(new_nodes) - def resolve_anyref( - self, - refdoc: str, - node: pending_xref, - contnode: Element, - ) -> Element | None: - """Resolve reference generated by the "any" role.""" - stddomain = self.env.domains.standard_domain + def _resolve_pending_xref( + self, node: addnodes.pending_xref, contnode: Element + ) -> nodes.reference | None: + new_node: nodes.reference | None + typ = node['reftype'] target = node['reftarget'] + ref_doc = node.setdefault('refdoc', self.env.docname) + ref_domain = node.get('refdomain', '') + domain: Domain | None + if ref_domain: + try: + domain = self.env.domains[ref_domain] + except KeyError: + return None + else: + domain = None + + try: + new_node = self._resolve_pending_xref_in_domain( + domain=domain, + node=node, + contnode=contnode, + ref_doc=ref_doc, + typ=typ, + target=target, + ) + except NoUri: + return None + if new_node is not None: + return new_node + + try: + # no new node found? try the missing-reference event + new_node = self.app.emit_firstresult( + 'missing-reference', + self.env, + node, + contnode, + allowed_exceptions=(NoUri,), + ) + except NoUri: + return None + if new_node is not None: + return new_node + + # Still not found? Emit a warning if we are in nitpicky mode + # or if the node wishes to be warned about. + self.warn_missing_reference(ref_doc, typ, target, node, domain) + return None + + def _resolve_pending_xref_in_domain( + self, + *, + domain: Domain | None, + node: addnodes.pending_xref, + contnode: Element, + ref_doc: str, + typ: str, + target: str, + ) -> nodes.reference | None: + # let the domain try to resolve the reference + if domain is not None: + return domain.resolve_xref( + self.env, ref_doc, self.app.builder, typ, target, node, contnode + ) + + # really hardwired reference types + if typ == 'any': + return self._resolve_pending_any_xref( + node=node, contnode=contnode, ref_doc=ref_doc, target=target + ) + + return None + + def _resolve_pending_any_xref( + self, + *, + node: addnodes.pending_xref, + contnode: Element, + ref_doc: str, + target: str, + ) -> nodes.reference | None: + """Resolve reference generated by the "any" role.""" + env = self.env + builder = self.app.builder + domains = env.domains + results: list[tuple[str, nodes.reference]] = [] # first, try resolving as :doc: - doc_ref = stddomain.resolve_xref( - self.env, refdoc, self.app.builder, 'doc', target, node, contnode + doc_ref = domains.standard_domain.resolve_xref( + env, ref_doc, builder, 'doc', target, node, contnode ) if doc_ref: results.append(('doc', doc_ref)) # next, do the standard domain (makes this a priority) - results.extend( - stddomain.resolve_any_xref( - self.env, refdoc, self.app.builder, target, node, contnode - ) + results += domains.standard_domain.resolve_any_xref( + env, ref_doc, builder, target, node, contnode ) - for domain in self.env.domains.sorted(): + for domain in domains.sorted(): if domain.name == 'std': continue # we did this one already try: - results.extend( - domain.resolve_any_xref( - self.env, refdoc, self.app.builder, target, node, contnode - ) + results += domain.resolve_any_xref( + env, ref_doc, builder, target, node, contnode ) except NotImplementedError: # the domain doesn't yet support the new interface # we have to manually collect possible references (SLOW) for role in domain.roles: res = domain.resolve_xref( - self.env, refdoc, self.app.builder, role, target, node, contnode + env, ref_doc, builder, role, target, node, contnode ) if res and len(res) > 0 and isinstance(res[0], nodes.Element): results.append((f'{domain.name}:{role}', res)) @@ -180,29 +216,23 @@ class ReferencesResolver(SphinxPostTransform): return f':{name}:`{reftitle}`' candidates = ' or '.join(starmap(stringify, results)) - logger.warning( - __( - "more than one target found for 'any' cross-" - 'reference %r: could be %s' - ), - target, - candidates, - location=node, - type='ref', - subtype='any', + msg = __( + "more than one target found for 'any' cross-reference %r: could be %s" ) - res_role, newnode = results[0] + logger.warning( + msg, target, candidates, location=node, type='ref', subtype='any' + ) + res_role, new_node = results[0] # Override "any" class with the actual role type to get the styling # approximately correct. - res_domain = res_role.split(':')[0] + res_domain = res_role.partition(':')[0] if ( - len(newnode) > 0 - and isinstance(newnode[0], nodes.Element) - and newnode[0].get('classes') + len(new_node) > 0 + and isinstance(new_node[0], nodes.Element) + and new_node[0].get('classes') ): - newnode[0]['classes'].append(res_domain) - newnode[0]['classes'].append(res_role.replace(':', '-')) - return newnode + new_node[0]['classes'].extend((res_domain, res_role.replace(':', '-'))) + return new_node def warn_missing_reference( self,