Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply qualname_overrides in more circumstances #191

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
23 changes: 23 additions & 0 deletions src/scanpydoc/elegant_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -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."""
Expand All @@ -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

Expand Down
42 changes: 42 additions & 0 deletions tests/test_elegant_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand Down
Loading