Skip to content

Commit 1d1ed18

Browse files
authored
Support generics (#158)
1 parent b0fe50f commit 1d1ed18

File tree

3 files changed

+73
-16
lines changed

3 files changed

+73
-16
lines changed

.vscode/launch.json

+13
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,18 @@
2323
"console": "internalConsole",
2424
"justMyCode": false,
2525
},
26+
{
27+
"name": "Debug test",
28+
"type": "debugpy",
29+
"request": "launch",
30+
"program": "${file}",
31+
"pythonArgs": ["-Xfrozen_modules=off"],
32+
"console": "internalConsole",
33+
"justMyCode": false,
34+
"purpose": ["debug-test"],
35+
"presentation": {
36+
"hidden": true,
37+
},
38+
},
2639
],
2740
}

src/scanpydoc/elegant_typehints/_formatting.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
import sys
44
import inspect
5-
from typing import TYPE_CHECKING
5+
from types import GenericAlias
6+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, get_args, get_origin
7+
8+
from sphinx_autodoc_typehints import format_annotation
9+
10+
from scanpydoc import elegant_typehints
11+
12+
13+
if TYPE_CHECKING:
14+
from sphinx.config import Config
615

716

817
if sys.version_info >= (3, 10):
@@ -11,13 +20,7 @@
1120
UnionType = None
1221

1322

14-
from scanpydoc import elegant_typehints
15-
16-
17-
if TYPE_CHECKING:
18-
from typing import Any
19-
20-
from sphinx.config import Config
23+
_GenericAlias: type = type(Generic[TypeVar("_")])
2124

2225

2326
def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
@@ -42,6 +45,11 @@ def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
4245

4346
tilde = "" if config.typehints_fully_qualified else "~"
4447

48+
if isinstance(annotation, (GenericAlias, _GenericAlias)):
49+
args = get_args(annotation)
50+
annotation = cast(type[Any], get_origin(annotation))
51+
else:
52+
args = None
4553
annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
4654
if annotation_cls.__module__ in {"typing", "types"}:
4755
return None
@@ -50,8 +58,15 @@ def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
5058
if inspect.isclass(annotation):
5159
full_name = f"{annotation.__module__}.{annotation.__qualname__}"
5260
override = elegant_typehints.qualname_overrides.get(full_name)
53-
role = "exc" if issubclass(annotation_cls, BaseException) else "class"
5461
if override is not None:
55-
return f":py:{role}:`{tilde}{override}`"
62+
role = "exc" if issubclass(annotation_cls, BaseException) else "class"
63+
if args is None:
64+
formatted_args = ""
65+
else:
66+
formatted_args = ", ".join(
67+
format_annotation(arg, config) for arg in args
68+
)
69+
formatted_args = rf"\ \[{formatted_args}]"
70+
return f":py:{role}:`{tilde}{override}`{formatted_args}"
5671

5772
return None

tests/test_elegant_typehints.py

+35-6
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ def testmod(make_module: Callable[[str, str], ModuleType]) -> ModuleType:
4545
return make_module(
4646
"testmod",
4747
"""\
48+
from __future__ import annotations
49+
from typing import Generic, TypeVar
50+
4851
class Class: pass
4952
class SubCl(Class): pass
5053
class Excep(RuntimeError): pass
5154
class Excep2(Excep): pass
55+
56+
T = TypeVar('T')
57+
class Gen(Generic[T]): pass
5258
""",
5359
)
5460

@@ -68,6 +74,7 @@ def app(make_app_setup: Callable[..., Sphinx]) -> Sphinx:
6874
"testmod.SubCl": "test.SubCl",
6975
"testmod.Excep": "test.Excep",
7076
"testmod.Excep2": "test.Excep2",
77+
"testmod.Gen": "test.Gen",
7178
},
7279
)
7380

@@ -209,14 +216,36 @@ def fn_test(m: Mapping[str, int] = {}) -> None: # pragma: no cover
209216
]
210217

211218

212-
def test_qualname_overrides_class(app: Sphinx, testmod: ModuleType) -> None:
213-
assert testmod.Class.__module__ == "testmod"
214-
assert typehints_formatter(testmod.Class, app.config) == ":py:class:`~test.Class`"
219+
@pytest.mark.parametrize(
220+
("get", "expected"),
221+
[
222+
pytest.param(lambda m: m.Class, ":py:class:`~test.Class`", id="class"),
223+
pytest.param(lambda m: m.Excep, ":py:exc:`~test.Excep`", id="exc"),
224+
pytest.param(
225+
lambda m: m.Gen[m.Class],
226+
r":py:class:`~test.Gen`\ \[:py:class:`~test.Class`]",
227+
id="generic",
228+
),
229+
],
230+
)
231+
def test_qualname_overrides(
232+
process_doc: ProcessDoc,
233+
testmod: ModuleType,
234+
get: Callable[[ModuleType], object],
235+
expected: str,
236+
) -> None:
237+
def fn_test(m: object) -> None: # pragma: no cover
238+
""":param m: Test M"""
239+
del m
215240

241+
fn_test.__annotations__["m"] = get(testmod)
242+
assert fn_test.__annotations__["m"].__module__ == "testmod"
216243

217-
def test_qualname_overrides_exception(app: Sphinx, testmod: ModuleType) -> None:
218-
assert testmod.Excep.__module__ == "testmod"
219-
assert typehints_formatter(testmod.Excep, app.config) == ":py:exc:`~test.Excep`"
244+
assert process_doc(fn_test) == [
245+
f":type m: {_escape_sat(expected)}",
246+
":param m: Test M",
247+
NONE_RTYPE,
248+
]
220249

221250

222251
# These guys aren’t listed as classes in Python’s intersphinx index:

0 commit comments

Comments
 (0)