From 75787702ea8e6b5b23fe3d0c4c621a13eb12ffb4 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 16 Jan 2025 10:18:22 +0100 Subject: [PATCH] Apply qualname_overrides in more circumstances (#191) --- .vscode/settings.json | 2 - src/scanpydoc/elegant_typehints/__init__.py | 23 +++++++++++ tests/test_elegant_typehints.py | 42 +++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 59ffaec..e0eb931 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,7 @@ { "python.analysis.typeCheckingMode": "strict", "python.testing.pytestArgs": ["-vv", "--color=yes"], - "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.terminal.activateEnvironment": false, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, diff --git a/src/scanpydoc/elegant_typehints/__init__.py b/src/scanpydoc/elegant_typehints/__init__.py index 870901b..0584556 100644 --- a/src/scanpydoc/elegant_typehints/__init__.py +++ b/src/scanpydoc/elegant_typehints/__init__.py @@ -68,7 +68,10 @@ def x() -> Tuple[int, float]: from collections.abc import Callable from sphinx.config import Config + from docutils.nodes import TextElement, reference + from sphinx.addnodes import pending_xref from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment __all__ = [ @@ -113,6 +116,24 @@ class PickleableCallable: __call__ = property(lambda self: self.func) +# https://www.sphinx-doc.org/en/master/extdev/event_callbacks.html#event-missing-reference +def _last_resolve( + app: Sphinx, + env: BuildEnvironment, + node: pending_xref, + contnode: TextElement, +) -> reference | None: + if "sphinx.ext.intersphinx" not in app.extensions: + return None + + from sphinx.ext.intersphinx import resolve_reference_detect_inventory + + if (qualname := qualname_overrides.get(node["reftarget"])) is None: + return None + node["reftarget"] = qualname + return resolve_reference_detect_inventory(env, node, contnode) + + @_setup_sig def setup(app: Sphinx) -> dict[str, Any]: """Patches :mod:`sphinx_autodoc_typehints` for a more elegant display.""" @@ -123,6 +144,8 @@ def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value("qualname_overrides", default={}, rebuild="html") app.add_config_value("annotate_defaults", default=True, rebuild="html") app.connect("config-inited", _init_vars) + # Add 1 to priority to run after sphinx.ext.intersphinx + app.connect("missing-reference", _last_resolve, priority=501) from ._formatting import typehints_formatter diff --git a/tests/test_elegant_typehints.py b/tests/test_elegant_typehints.py index cdc7cd2..0bee9ea 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -12,12 +12,14 @@ import pytest +from scanpydoc.elegant_typehints import _last_resolve, qualname_overrides from scanpydoc.elegant_typehints._formatting import typehints_formatter if TYPE_CHECKING: from types import ModuleType from typing import Protocol + from collections.abc import Generator from sphinx.application import Sphinx @@ -32,6 +34,12 @@ def __call__( # noqa: D102 NONE_RTYPE = ":rtype: :sphinx_autodoc_typehints_type:`\\:py\\:obj\\:\\`None\\``" +@pytest.fixture(autouse=True) +def _reset_qualname_overrides() -> Generator[None, None, None]: + yield + qualname_overrides.clear() + + @pytest.fixture def testmod(make_module: Callable[[str, str], ModuleType]) -> ModuleType: return make_module( @@ -240,6 +248,40 @@ def fn_test(m: object) -> None: # pragma: no cover ] +def test_resolve(app: Sphinx) -> None: + """Test that qualname_overrides affects _last_resolve as expected.""" + from docutils.nodes import TextElement, reference + from sphinx.addnodes import pending_xref + from sphinx.ext.intersphinx import InventoryAdapter + + app.setup_extension("sphinx.ext.intersphinx") + + # Inventory contains documented name + InventoryAdapter(app.env).main_inventory["py:class"] = { + "test.Class": ("TestProj", "1", "https://x.com", "Class"), + } + # Node contains name from code + node = pending_xref(refdomain="py", reftarget="testmod.Class", reftype="class") + + resolved = _last_resolve(app, app.env, node, TextElement()) + assert isinstance(resolved, reference) + assert resolved["refuri"] == "https://x.com" + assert resolved["reftitle"] == "(in TestProj v1)" + + +@pytest.mark.parametrize("qualname", ["testmod.Class", "nonexistent.Class"]) +def test_resolve_failure(app: Sphinx, qualname: str) -> None: + from docutils.nodes import TextElement + from sphinx.addnodes import pending_xref + + app.setup_extension("sphinx.ext.intersphinx") + node = pending_xref(refdomain="py", reftarget=qualname, reftype="class") + + resolved = _last_resolve(app, app.env, node, TextElement()) + assert resolved is None + assert node["reftarget"] == qualname_overrides.get(qualname, qualname) + + # These guys aren’t listed as classes in Python’s intersphinx index: @pytest.mark.parametrize( "annotation",