diff --git a/CHANGELOG.md b/CHANGELOG.md index 907e1e1a..2f6ea3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.20 - Use hatchling instead of setuptools +- Add support for typing.ParamSpec ## 1.19.2 diff --git a/pyproject.toml b/pyproject.toml index ab792c78..0e4bbe09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ optional-dependencies.testing = [ "sphobjinv>=2.2.2", "typing-extensions>=4.3", ] -optional-dependencies.type_comment = ['typed-ast>=1.5.4; python_version < "3.8"'] +optional-dependencies.type-comment = ['typed-ast>=1.5.4; python_version < "3.8"'] dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index 753921be..32e81240 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -26,7 +26,7 @@ def get_annotation_module(annotation: Any) -> str: if annotation is None: return "builtins" is_new_type = sys.version_info >= (3, 10) and isinstance(annotation, NewType) - if is_new_type or isinstance(annotation, TypeVar): + if is_new_type or isinstance(annotation, TypeVar) or type(annotation).__name__ == "ParamSpec": return "typing" if hasattr(annotation, "__module__"): return annotation.__module__ # type: ignore # deduced Any @@ -63,8 +63,8 @@ def get_annotation_class_name(annotation: Any, module: str) -> str: elif getattr(origin, "_name", None): # Required for Union on Python 3.7+ return origin._name # type: ignore # deduced Any - annotation_cls = annotation if inspect.isclass(annotation) else annotation.__class__ - return annotation_cls.__qualname__.lstrip("_") # type: ignore # deduced Any + annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) + return annotation_cls.__qualname__.lstrip("_") def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[Any, ...]: @@ -153,7 +153,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901 # t if full_name == "typing.NewType": args_format = f"\\(``{annotation.__name__}``, {{}})" role = "class" if sys.version_info >= (3, 10) else "func" - elif full_name == "typing.TypeVar": + elif full_name in {"typing.TypeVar", "typing.ParamSpec"}: params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")} params = {k: v for k, v in params.items() if v} if "bound" in params: diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index dda77fa6..48013eea 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -56,6 +56,10 @@ Z = TypeVar("Z", bound="A") S = TypeVar("S", bound="miss") # type: ignore # miss not defined on purpose # noqa: F821 W = NewType("W", str) +P = typing_extensions.ParamSpec("P") +P_co = typing_extensions.ParamSpec("P_co", covariant=True) # type: ignore +P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True) # type: ignore +P_bound = typing_extensions.ParamSpec("P_bound", bound=str) # type: ignore # Mypy does not support recursive type aliases, but # other type checkers do. @@ -239,6 +243,11 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t (Y, ":py:class:`~typing.TypeVar`\\(``Y``, bound= :py:class:`str`)"), (Z, ":py:class:`~typing.TypeVar`\\(``Z``, bound= A)"), (S, ":py:class:`~typing.TypeVar`\\(``S``, bound= miss)"), + # ParamSpec should behave like TypeVar, except for missing constraints + (P, ":py:class:`~typing.ParamSpec`\\(``P``)"), + (P_co, ":py:class:`~typing.ParamSpec`\\(``P_co``, covariant=True)"), + (P_contra, ":py:class:`~typing.ParamSpec`\\(``P_contra``, contravariant=True)"), + (P_bound, ":py:class:`~typing.ParamSpec`\\(``P_bound``, bound= :py:class:`str`)"), # ## These test for correct internal tuple rendering, even if not all are valid Tuple types # Zero-length tuple remains (Tuple[()], ":py:data:`~typing.Tuple`"), diff --git a/tox.ini b/tox.ini index dbc09722..4f80f90f 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ setenv = COVERAGE_FILE = {toxworkdir}{/}.coverage.{envname} extras = testing - type_comments + type-comment commands = pytest {tty:--color=yes} {posargs: \ --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}sphinx_autodoc_typehints --cov {toxinidir}{/}tests \