diff --git a/src/scanpydoc/_types.py b/src/scanpydoc/_types.py new file mode 100644 index 0000000..6c06f18 --- /dev/null +++ b/src/scanpydoc/_types.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from typing import Generic, TypeVar + + +_GenericAlias: type = type(Generic[TypeVar("_")]) diff --git a/src/scanpydoc/elegant_typehints/_formatting.py b/src/scanpydoc/elegant_typehints/_formatting.py index f256c54..24edb3e 100644 --- a/src/scanpydoc/elegant_typehints/_formatting.py +++ b/src/scanpydoc/elegant_typehints/_formatting.py @@ -3,11 +3,12 @@ import sys import inspect from types import GenericAlias -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, get_args, get_origin +from typing import TYPE_CHECKING, Any, cast, get_args, get_origin from sphinx_autodoc_typehints import format_annotation from scanpydoc import elegant_typehints +from scanpydoc._types import _GenericAlias if TYPE_CHECKING: @@ -20,9 +21,6 @@ UnionType = None -_GenericAlias: type = type(Generic[TypeVar("_")]) - - def typehints_formatter(annotation: type[Any], config: Config) -> str | None: """Generate reStructuredText containing links to the types. diff --git a/src/scanpydoc/rtd_github_links/__init__.py b/src/scanpydoc/rtd_github_links/__init__.py index ced719a..5a44ad5 100644 --- a/src/scanpydoc/rtd_github_links/__init__.py +++ b/src/scanpydoc/rtd_github_links/__init__.py @@ -61,7 +61,7 @@ import sys import inspect -from types import ModuleType +from types import ModuleType, GenericAlias from typing import TYPE_CHECKING from pathlib import Path, PurePosixPath from importlib import import_module @@ -70,6 +70,7 @@ from jinja2.defaults import DEFAULT_FILTERS # type: ignore[attr-defined] from scanpydoc import metadata, _setup_sig +from scanpydoc._types import _GenericAlias if TYPE_CHECKING: @@ -158,7 +159,7 @@ def _get_obj_module(qualname: str) -> tuple[Any, ModuleType]: raise e from None if isinstance(thing, ModuleType): # pragma: no cover mod = thing - elif is_dataclass(obj): + elif is_dataclass(obj) or isinstance(thing, (GenericAlias, _GenericAlias)): obj = thing else: obj = thing @@ -186,7 +187,8 @@ def _module_path(obj: _SourceObjectType, module: ModuleType) -> PurePosixPath: try: file = Path(inspect.getabsfile(obj)) except TypeError: - file = Path(module.__file__ or "") + # Some don’t have the attribute, some have it set to None + file = Path(getattr(module, "__file__", None) or "") offset = -1 if file.name == "__init__.py" else 0 parts = module.__name__.split(".") return PurePosixPath(*file.parts[offset - len(parts) :]) diff --git a/src/scanpydoc/rtd_github_links/_testdata.py b/src/scanpydoc/rtd_github_links/_testdata.py index e20d6a8..4fe3430 100644 --- a/src/scanpydoc/rtd_github_links/_testdata.py +++ b/src/scanpydoc/rtd_github_links/_testdata.py @@ -2,11 +2,31 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Generic, TypeVar from dataclasses import field, dataclass from legacy_api_wrap import legacy_api +if TYPE_CHECKING: + from typing import TypeAlias + + +_T = TypeVar("_T") + + +class _G(Generic[_T]): + pass + + +# make sure that TestGenericClass keeps its __module__ +_G.__module__ = "somewhere_else" + + +TestGenericBuiltin: TypeAlias = list[str] +TestGenericClass: TypeAlias = _G[int] + + @dataclass class TestDataCls: test_attr: dict[str, str] = field(default_factory=dict) diff --git a/tests/test_rtd_github_links.py b/tests/test_rtd_github_links.py index 05ad219..45658eb 100644 --- a/tests/test_rtd_github_links.py +++ b/tests/test_rtd_github_links.py @@ -204,6 +204,20 @@ def test_get_github_url_error() -> None: "scanpydoc/rtd_github_links/_testdata.py", id="anno", ), + pytest.param( + "scanpydoc.rtd_github_links._testdata.TestGenericBuiltin", + _testdata.TestGenericBuiltin, + _testdata, + "scanpydoc/rtd_github_links/_testdata.py", + id="generic_builtin", + ), + pytest.param( + "scanpydoc.rtd_github_links._testdata.TestGenericClass", + _testdata.TestGenericClass, + _testdata, + "scanpydoc/rtd_github_links/_testdata.py", + id="generic_class", + ), ], ) def test_get_obj_module_path(