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,
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:

View File

@ -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

View File

@ -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,