Skip to content

Commit

Permalink
Support generics (#158)
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep authored Jul 9, 2024
1 parent b0fe50f commit 1d1ed18
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 16 deletions.
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,18 @@
"console": "internalConsole",
"justMyCode": false,
},
{
"name": "Debug test",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"pythonArgs": ["-Xfrozen_modules=off"],
"console": "internalConsole",
"justMyCode": false,
"purpose": ["debug-test"],
"presentation": {
"hidden": true,
},
},
],
}
35 changes: 25 additions & 10 deletions src/scanpydoc/elegant_typehints/_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

import sys
import inspect
from typing import TYPE_CHECKING
from types import GenericAlias
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, get_args, get_origin

from sphinx_autodoc_typehints import format_annotation

from scanpydoc import elegant_typehints


if TYPE_CHECKING:
from sphinx.config import Config


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


from scanpydoc import elegant_typehints


if TYPE_CHECKING:
from typing import Any

from sphinx.config import Config
_GenericAlias: type = type(Generic[TypeVar("_")])


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

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

if isinstance(annotation, (GenericAlias, _GenericAlias)):
args = get_args(annotation)
annotation = cast(type[Any], get_origin(annotation))
else:
args = None
annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
if annotation_cls.__module__ in {"typing", "types"}:
return None
Expand All @@ -50,8 +58,15 @@ def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
if inspect.isclass(annotation):
full_name = f"{annotation.__module__}.{annotation.__qualname__}"
override = elegant_typehints.qualname_overrides.get(full_name)
role = "exc" if issubclass(annotation_cls, BaseException) else "class"
if override is not None:
return f":py:{role}:`{tilde}{override}`"
role = "exc" if issubclass(annotation_cls, BaseException) else "class"
if args is None:
formatted_args = ""
else:
formatted_args = ", ".join(
format_annotation(arg, config) for arg in args
)
formatted_args = rf"\ \[{formatted_args}]"
return f":py:{role}:`{tilde}{override}`{formatted_args}"

return None
41 changes: 35 additions & 6 deletions tests/test_elegant_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ def testmod(make_module: Callable[[str, str], ModuleType]) -> ModuleType:
return make_module(
"testmod",
"""\
from __future__ import annotations
from typing import Generic, TypeVar
class Class: pass
class SubCl(Class): pass
class Excep(RuntimeError): pass
class Excep2(Excep): pass
T = TypeVar('T')
class Gen(Generic[T]): pass
""",
)

Expand All @@ -68,6 +74,7 @@ def app(make_app_setup: Callable[..., Sphinx]) -> Sphinx:
"testmod.SubCl": "test.SubCl",
"testmod.Excep": "test.Excep",
"testmod.Excep2": "test.Excep2",
"testmod.Gen": "test.Gen",
},
)

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


def test_qualname_overrides_class(app: Sphinx, testmod: ModuleType) -> None:
assert testmod.Class.__module__ == "testmod"
assert typehints_formatter(testmod.Class, app.config) == ":py:class:`~test.Class`"
@pytest.mark.parametrize(
("get", "expected"),
[
pytest.param(lambda m: m.Class, ":py:class:`~test.Class`", id="class"),
pytest.param(lambda m: m.Excep, ":py:exc:`~test.Excep`", id="exc"),
pytest.param(
lambda m: m.Gen[m.Class],
r":py:class:`~test.Gen`\ \[:py:class:`~test.Class`]",
id="generic",
),
],
)
def test_qualname_overrides(
process_doc: ProcessDoc,
testmod: ModuleType,
get: Callable[[ModuleType], object],
expected: str,
) -> None:
def fn_test(m: object) -> None: # pragma: no cover
""":param m: Test M"""
del m

fn_test.__annotations__["m"] = get(testmod)
assert fn_test.__annotations__["m"].__module__ == "testmod"

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


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

0 comments on commit 1d1ed18

Please sign in to comment.