Refactor `ReferencesResolver` (#13321)

Split ``ReferencesResolver.run()`` into smaller parts.
This commit is contained in:
Adam Turner 2025-02-09 21:38:51 +00:00 committed by GitHub
parent 03df8119b3
commit 9eb5097a56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 95 deletions

View File

@ -30,7 +30,8 @@ if TYPE_CHECKING:
InventoryName, InventoryName,
InventoryURI, 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: def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None:

View File

@ -31,7 +31,7 @@ if TYPE_CHECKING:
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.ext.intersphinx._shared import InventoryName from sphinx.ext.intersphinx._shared import InventoryName
from sphinx.util.inventory import _InventoryItem 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( def _create_element_from_result(
@ -75,7 +75,7 @@ def _create_element_from_result(
def _resolve_reference_in_domain_by_target( def _resolve_reference_in_domain_by_target(
inv_name: InventoryName | None, inv_name: InventoryName | None,
inventory: _Inventory, inventory: Inventory,
domain_name: str, domain_name: str,
objtypes: Iterable[str], objtypes: Iterable[str],
target: str, target: str,
@ -139,7 +139,7 @@ def _resolve_reference_in_domain_by_target(
def _resolve_reference_in_domain( def _resolve_reference_in_domain(
inv_name: InventoryName | None, inv_name: InventoryName | None,
inventory: _Inventory, inventory: Inventory,
honor_disabled_refs: bool, honor_disabled_refs: bool,
disabled_reftypes: Set[str], disabled_reftypes: Set[str],
domain: Domain, domain: Domain,
@ -190,7 +190,7 @@ def _resolve_reference_in_domain(
def _resolve_reference( def _resolve_reference(
inv_name: InventoryName | None, inv_name: InventoryName | None,
domains: _DomainsContainer, domains: _DomainsContainer,
inventory: _Inventory, inventory: Inventory,
honor_disabled_refs: bool, honor_disabled_refs: bool,
disabled_reftypes: Set[str], disabled_reftypes: Set[str],
node: pending_xref, node: pending_xref,
@ -305,7 +305,7 @@ def resolve_reference_detect_inventory(
Resolution is tried first with the target as is in any 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 ``:``, 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. is tried in that inventory with the new target.
""" """
# ordinary direct lookup, use data as is # ordinary direct lookup, use data as is
@ -317,10 +317,10 @@ def resolve_reference_detect_inventory(
target = node['reftarget'] target = node['reftarget']
if ':' not in target: if ':' not in target:
return None return None
inv_name, newtarget = target.split(':', 1) inv_name, _, new_target = target.partition(':')
if not inventory_exists(env, inv_name): if not inventory_exists(env, inv_name):
return None return None
node['reftarget'] = newtarget node['reftarget'] = new_target
res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode) res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode)
node['reftarget'] = target node['reftarget'] = target
return res_inv return res_inv

View File

@ -72,101 +72,137 @@ class ReferencesResolver(SphinxPostTransform):
else: else:
contnode = cast('Element', node[0].deepcopy()) contnode = cast('Element', node[0].deepcopy())
newnode = None new_node = self._resolve_pending_xref(node, contnode)
if new_node:
typ = node['reftype'] new_nodes: list[Node] = [new_node]
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]
else: else:
newnodes = [contnode] new_nodes = [contnode]
if newnode is None and isinstance( if new_node is None and isinstance(
node[0], addnodes.pending_xref_condition node[0], addnodes.pending_xref_condition
): ):
matched = self.find_pending_xref_condition(node, ('*',)) matched = self.find_pending_xref_condition(node, ('*',))
if matched: if matched:
newnodes = matched new_nodes = matched
else: else:
logger.warning( msg = __(
__( 'Could not determine the fallback text for the '
'Could not determine the fallback text for the ' 'cross-reference. Might be a bug.'
'cross-reference. Might be a bug.'
),
location=node,
) )
logger.warning(msg, location=node)
node.replace_self(newnodes) node.replace_self(new_nodes)
def resolve_anyref( def _resolve_pending_xref(
self, self, node: addnodes.pending_xref, contnode: Element
refdoc: str, ) -> nodes.reference | None:
node: pending_xref, new_node: nodes.reference | None
contnode: Element, typ = node['reftype']
) -> Element | None:
"""Resolve reference generated by the "any" role."""
stddomain = self.env.domains.standard_domain
target = node['reftarget'] 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]] = [] results: list[tuple[str, nodes.reference]] = []
# first, try resolving as :doc: # first, try resolving as :doc:
doc_ref = stddomain.resolve_xref( doc_ref = domains.standard_domain.resolve_xref(
self.env, refdoc, self.app.builder, 'doc', target, node, contnode env, ref_doc, builder, 'doc', target, node, contnode
) )
if doc_ref: if doc_ref:
results.append(('doc', doc_ref)) results.append(('doc', doc_ref))
# next, do the standard domain (makes this a priority) # next, do the standard domain (makes this a priority)
results.extend( results += domains.standard_domain.resolve_any_xref(
stddomain.resolve_any_xref( env, ref_doc, builder, target, node, contnode
self.env, refdoc, self.app.builder, target, node, contnode
)
) )
for domain in self.env.domains.sorted(): for domain in domains.sorted():
if domain.name == 'std': if domain.name == 'std':
continue # we did this one already continue # we did this one already
try: try:
results.extend( results += domain.resolve_any_xref(
domain.resolve_any_xref( env, ref_doc, builder, target, node, contnode
self.env, refdoc, self.app.builder, target, node, contnode
)
) )
except NotImplementedError: except NotImplementedError:
# the domain doesn't yet support the new interface # the domain doesn't yet support the new interface
# we have to manually collect possible references (SLOW) # we have to manually collect possible references (SLOW)
for role in domain.roles: for role in domain.roles:
res = domain.resolve_xref( 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): if res and len(res) > 0 and isinstance(res[0], nodes.Element):
results.append((f'{domain.name}:{role}', res)) results.append((f'{domain.name}:{role}', res))
@ -180,29 +216,23 @@ class ReferencesResolver(SphinxPostTransform):
return f':{name}:`{reftitle}`' return f':{name}:`{reftitle}`'
candidates = ' or '.join(starmap(stringify, results)) candidates = ' or '.join(starmap(stringify, results))
logger.warning( msg = __(
__( "more than one target found for 'any' cross-reference %r: could be %s"
"more than one target found for 'any' cross-"
'reference %r: could be %s'
),
target,
candidates,
location=node,
type='ref',
subtype='any',
) )
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 # Override "any" class with the actual role type to get the styling
# approximately correct. # approximately correct.
res_domain = res_role.split(':')[0] res_domain = res_role.partition(':')[0]
if ( if (
len(newnode) > 0 len(new_node) > 0
and isinstance(newnode[0], nodes.Element) and isinstance(new_node[0], nodes.Element)
and newnode[0].get('classes') and new_node[0].get('classes')
): ):
newnode[0]['classes'].append(res_domain) new_node[0]['classes'].extend((res_domain, res_role.replace(':', '-')))
newnode[0]['classes'].append(res_role.replace(':', '-')) return new_node
return newnode
def warn_missing_reference( def warn_missing_reference(
self, self,