Source code for genno.compat.sphinx.rewrite_refs

"""Resolve missing references using aliased target names, domains, and/or types.

Expanded from and with thanks for https://stackoverflow.com/a/62301461.
"""

import re
from typing import TYPE_CHECKING, Mapping, Optional

from docutils.nodes import Text
from sphinx.addnodes import pending_xref
from sphinx.ext.intersphinx import missing_reference

if TYPE_CHECKING:
    import sphinx.application


class Replacement:
    refdomain: Optional[str]
    reftype: Optional[str]
    reftarget: str
    text: Optional[str]

    # Identifier characters
    c = "[^`<>]+"

    # Match any of:
    # - ":domain:type:`text <target>`"
    # - ":domain:type:`target`"
    # - ":type:`text <target>`"
    # - ":type:`target`"
    # - "text <target>"
    # - "target"
    _target_expr = re.compile(
        rf"(:((?P<rd>{c}):)?(?P<rt>{c}):)?(`?)(?P<t_or_t>{c})(<(?P<target>{c})>)?\5"
    )

    def __init__(self, value: str) -> None:
        match = self._target_expr.fullmatch(value)
        assert match is not None
        self.refdomain, self.reftype, target_or_text, target = [
            match.group(k) for k in ("rd", "rt", "t_or_t", "target")
        ]
        if target is None:  # Target only, no replacement text
            self.reftarget, self.text = target_or_text, None
        else:  # Both target and text replacement
            self.reftarget, self.text = target, target_or_text.rstrip()


[docs] def apply_alias(config: Mapping[str, str], node) -> bool: """Apply `config` to `node`.""" try: # Identify an alias expression matching the "reftarget" attribute of `node` expr = next(filter(lambda e: re.match(e, node["reftarget"]), config)) except (KeyError, StopIteration): # No such attribute, or no matching expression → nothing to do return False # Unpack information about the replacement replace = Replacement(config[expr]) # Resolve the ref by substituting the reftarget for the matching part node["reftarget"] = re.sub(f"^{expr}", replace.reftarget, node["reftarget"]) # Rewrite the rendered text, reftype, and refdomain if replace.text: # Find the text node child text_node = next(iter(node.traverse(lambda n: n.tagname == "#text"))) # Remove the old text node, add new text node with custom text text_node.parent.replace(text_node, Text(replace.text)) # Force further processing to preserve this text node node["refexplicit"] = True if replace.reftype: node["reftype"] = replace.reftype if replace.refdomain: node["refdomain"] = replace.refdomain return True
[docs] def resolve_internal_aliases(app: "sphinx.application.Sphinx", doctree): """Handler for 'doctree-read' events.""" config = app.config["reference_aliases"] for node in doctree.traverse(condition=pending_xref): apply_alias(config, node)
[docs] def resolve_intersphinx_aliases(app, env, node, contnode): """Handler for 'missing-reference' (intersphinx) events.""" if apply_alias(app.config["reference_aliases"], node): # Delegate the rest of the work to intersphinx return missing_reference(app, env, node, contnode)
[docs] def setup(app: "sphinx.application.Sphinx"): """Connect :mod:`.rewrite_refs` event handlers.""" app.add_config_value("reference_aliases", dict(), "") app.connect("doctree-read", resolve_internal_aliases) app.connect("missing-reference", resolve_intersphinx_aliases)