From 16a47d415007d20bcdded4e8ce3c45811ae8a45c Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 14 Jan 2025 15:57:35 +0100 Subject: [PATCH 1/6] Add more qualname resolve --- .vscode/settings.json | 2 -- src/scanpydoc/elegant_typehints/__init__.py | 20 +++++++++++++++ tests/test_elegant_typehints.py | 28 +++++++++++++++++++++ 3 files changed, 48 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..fa9b0c8 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,21 @@ 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, # noqa: ARG001 + env: BuildEnvironment, + node: pending_xref, + contnode: TextElement, +) -> reference | 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 +141,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..4da499d 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -12,6 +12,7 @@ import pytest +from scanpydoc.elegant_typehints import _last_resolve from scanpydoc.elegant_typehints._formatting import typehints_formatter @@ -240,6 +241,33 @@ 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( + refdoc="whatever", + refdomain="py", + reftarget="testmod.Class", + refspecific="False", + 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)" + + # These guys aren’t listed as classes in Python’s intersphinx index: @pytest.mark.parametrize( "annotation", From ea829ab5d09908e84bbaf3e52301bc50fa890005 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 14 Jan 2025 16:03:42 +0100 Subject: [PATCH 2/6] only if intersphinx is enabled --- src/scanpydoc/elegant_typehints/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scanpydoc/elegant_typehints/__init__.py b/src/scanpydoc/elegant_typehints/__init__.py index fa9b0c8..0584556 100644 --- a/src/scanpydoc/elegant_typehints/__init__.py +++ b/src/scanpydoc/elegant_typehints/__init__.py @@ -118,11 +118,14 @@ class PickleableCallable: # https://www.sphinx-doc.org/en/master/extdev/event_callbacks.html#event-missing-reference def _last_resolve( - app: Sphinx, # noqa: ARG001 + 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: From 80e2345f32af702d6fc34e2d7ad3ed0ae6bbad12 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 14 Jan 2025 16:28:27 +0100 Subject: [PATCH 3/6] coverage --- tests/test_elegant_typehints.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_elegant_typehints.py b/tests/test_elegant_typehints.py index 4da499d..7475337 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -254,13 +254,7 @@ def test_resolve(app: Sphinx) -> None: "test.Class": ("TestProj", "1", "https://x.com", "Class"), } # Node contains name from code - node = pending_xref( - refdoc="whatever", - refdomain="py", - reftarget="testmod.Class", - refspecific="False", - reftype="class", - ) + node = pending_xref(refdomain="py", reftarget="testmod.Class", reftype="class") resolved = _last_resolve(app, app.env, node, TextElement()) assert isinstance(resolved, reference) @@ -268,6 +262,17 @@ def test_resolve(app: Sphinx) -> None: assert resolved["reftitle"] == "(in TestProj v1)" +def test_resolve_failure(app: Sphinx) -> None: + from docutils.nodes import TextElement + from sphinx.addnodes import pending_xref + + app.setup_extension("sphinx.ext.intersphinx") + node = pending_xref(refdomain="py", reftarget="testmod.Class", reftype="class") + + resolved = _last_resolve(app, app.env, node, TextElement()) + assert resolved is None + + # These guys aren’t listed as classes in Python’s intersphinx index: @pytest.mark.parametrize( "annotation", From b7e877efbd9792df86a1f5a40603011d1ae0bc23 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 14 Jan 2025 16:46:14 +0100 Subject: [PATCH 4/6] reset --- tests/test_elegant_typehints.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_elegant_typehints.py b/tests/test_elegant_typehints.py index 7475337..1066048 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -12,13 +12,14 @@ import pytest -from scanpydoc.elegant_typehints import _last_resolve +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 @@ -33,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( From 5d89938b64561912cf6dad6ccbc1afd183f9156c Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 14 Jan 2025 17:18:52 +0100 Subject: [PATCH 5/6] better test --- tests/test_elegant_typehints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_elegant_typehints.py b/tests/test_elegant_typehints.py index 1066048..7ae913e 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -274,10 +274,11 @@ def test_resolve_failure(app: Sphinx) -> None: from sphinx.addnodes import pending_xref app.setup_extension("sphinx.ext.intersphinx") - node = pending_xref(refdomain="py", reftarget="testmod.Class", reftype="class") + node = pending_xref(refdomain="py", reftarget="nonexistent.Class", reftype="class") resolved = _last_resolve(app, app.env, node, TextElement()) assert resolved is None + assert node["reftarget"] == "nonexistent.Class", "reftarget got changed" # These guys aren’t listed as classes in Python’s intersphinx index: From 9800e53299f1a9a1d99627535d41d0d444b7aad9 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 14 Jan 2025 17:24:31 +0100 Subject: [PATCH 6/6] test both cases --- tests/test_elegant_typehints.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_elegant_typehints.py b/tests/test_elegant_typehints.py index 7ae913e..0bee9ea 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -269,16 +269,17 @@ def test_resolve(app: Sphinx) -> None: assert resolved["reftitle"] == "(in TestProj v1)" -def test_resolve_failure(app: Sphinx) -> None: +@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="nonexistent.Class", reftype="class") + node = pending_xref(refdomain="py", reftarget=qualname, reftype="class") resolved = _last_resolve(app, app.env, node, TextElement()) assert resolved is None - assert node["reftarget"] == "nonexistent.Class", "reftarget got changed" + assert node["reftarget"] == qualname_overrides.get(qualname, qualname) # These guys aren’t listed as classes in Python’s intersphinx index: