diff --git a/CHANGES.rst b/CHANGES.rst index 226e731cf8b..59df3070283 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,8 @@ Bugs fixed * Fix invalid HTML when a rubric node with invalid ``heading-level`` is used. Patch by Adam Turner. +* #12579, #12581: Restore support for ``typing.ParamSpec`` in autodoc. + Patch by Adam Turner. Testing ------- diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 28ca86490b5..a295c0605e6 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -212,7 +212,11 @@ def _is_unpack_form(obj: Any) -> bool: def _typing_internal_name(obj: Any) -> str | None: if sys.version_info[:2] >= (3, 10): - return obj.__name__ + try: + return obj.__name__ + except AttributeError: + # e.g. ParamSpecArgs, ParamSpecKwargs + return '' return getattr(obj, '_name', None) @@ -237,7 +241,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s raise ValueError(msg) # things that are not types - if cls in {None, NoneType}: + if cls is None or cls == NoneType: return ':py:obj:`None`' if cls is Ellipsis: return '...' @@ -388,7 +392,7 @@ def stringify_annotation( raise ValueError(msg) # things that are not types - if annotation in {None, NoneType}: + if annotation is None or annotation == NoneType: return 'None' if annotation is Ellipsis: return '...' diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 044b4599058..d00d69fb04f 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -385,6 +385,21 @@ def test_restify_mock(): assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`' +@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.') +def test_restify_type_hints_paramspec(): + from typing import ParamSpec + P = ParamSpec('P') + + assert restify(P) == ":py:obj:`tests.test_util.test_util_typing.P`" + assert restify(P, "smart") == ":py:obj:`~tests.test_util.test_util_typing.P`" + + assert restify(P.args) == "P.args" + assert restify(P.args, "smart") == "P.args" + + assert restify(P.kwargs) == "P.kwargs" + assert restify(P.kwargs, "smart") == "P.kwargs" + + def test_stringify_annotation(): assert stringify_annotation(int, 'fully-qualified-except-typing') == "int" assert stringify_annotation(int, "smart") == "int" @@ -722,3 +737,21 @@ def test_stringify_type_ForwardRef(): assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]]) == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined] assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'fully-qualified-except-typing') == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined] assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'smart') == "~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]" # type: ignore[attr-defined] + + +@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.') +def test_stringify_type_hints_paramspec(): + from typing import ParamSpec + P = ParamSpec('P') + + assert stringify_annotation(P, 'fully-qualified') == "~P" + assert stringify_annotation(P, 'fully-qualified-except-typing') == "~P" + assert stringify_annotation(P, "smart") == "~P" + + assert stringify_annotation(P.args, 'fully-qualified') == "typing.~P" + assert stringify_annotation(P.args, 'fully-qualified-except-typing') == "~P" + assert stringify_annotation(P.args, "smart") == "~typing.~P" + + assert stringify_annotation(P.kwargs, 'fully-qualified') == "typing.~P" + assert stringify_annotation(P.kwargs, 'fully-qualified-except-typing') == "~P" + assert stringify_annotation(P.kwargs, "smart") == "~typing.~P"