diff --git a/AUTHORS.rst b/AUTHORS.rst index ea363fd118f..f1f6d272dea 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -100,6 +100,7 @@ Contributors * Pauli Virtanen -- autodoc improvements, autosummary extension * Rafael Fontenelle -- internationalisation * \A. Rafey Khan -- improved intersphinx typing +* Rui Pinheiro -- Python 3.14 forward references support * Roland Meister -- epub builder * Sebastian Wiesner -- image handling, distutils support * Slawek Figiel -- additional warning suppression diff --git a/CHANGES.rst b/CHANGES.rst index 792f6ce2201..86d990cb846 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -132,6 +132,9 @@ Bugs fixed directly defined in certain cases, depending on autodoc processing order. Patch by Jeremy Maitin-Shepard. +* #13945: sphinx.ext.autodoc: Fix handling of Python 3.14 forward references in + annotations by using ```annotationlib.Format.FORWARDREF``. + Patch by Rui Pinheiro. Testing diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 65088c38d05..8dd1f32e370 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -72,6 +72,18 @@ def __call__(self, obj: Any, name: str, default: Any = ..., /) -> Any: ... isclass = inspect.isclass ismodule = inspect.ismodule +# Python 3.14 added the annotationlib module to the standard library as well as the +# 'annotation_format' keyword parameter to inspect.signature(), which allows us to handle +# forward references more robustly. +if sys.version_info[0:2] >= (3, 14): + import annotationlib # type: ignore[import-not-found] + + inspect_signature_extra: dict[str, Any] = { + 'annotation_format': annotationlib.Format.FORWARDREF + } +else: + inspect_signature_extra: dict[str, Any] = {} + def unwrap(obj: Any) -> Any: """Get an original object from wrapped object (wrapped functions). @@ -718,12 +730,16 @@ def signature( try: if _should_unwrap(subject): - signature = inspect.signature(subject) # type: ignore[arg-type] + signature = inspect.signature(subject, **inspect_signature_extra) # type: ignore[arg-type] else: - signature = inspect.signature(subject, follow_wrapped=True) # type: ignore[arg-type] + signature = inspect.signature( + subject, # type: ignore[arg-type] + follow_wrapped=True, + **inspect_signature_extra, + ) except ValueError: # follow built-in wrappers up (ex. functools.lru_cache) - signature = inspect.signature(subject) # type: ignore[arg-type] + signature = inspect.signature(subject, **inspect_signature_extra) # type: ignore[arg-type] parameters = list(signature.parameters.values()) return_annotation = signature.return_annotation diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 2ff37091fd2..0140df39c8f 100644 --- a/tests/test_util/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -106,6 +106,14 @@ def wrapper(): return wrapper +def forward_reference_in_args(x: Foo) -> None: # type: ignore[name-defined] # noqa: F821 + pass + + +def forward_reference_in_return() -> Foo: # type: ignore[name-defined] # noqa: F821 + pass + + def test_TypeAliasForwardRef(): alias = TypeAliasForwardRef('example') sig_str = stringify_annotation(alias, 'fully-qualified-except-typing') @@ -164,6 +172,13 @@ def func(a, b, c=1, d=2, *e, **f): sig = inspect.stringify_signature(inspect.signature(func)) assert sig == '(a, b, c=1, d=2, *e, **f)' + # forward references + sig = inspect.stringify_signature(inspect.signature(forward_reference_in_args)) + assert sig == '(x: Foo) -> None' + + sig = inspect.stringify_signature(inspect.signature(forward_reference_in_return)) + assert sig == '() -> Foo' + def test_signature_partial() -> None: def fun(a, b, c=1, d=2): diff --git a/tests/test_util/test_util_inspect_py314.py b/tests/test_util/test_util_inspect_py314.py new file mode 100644 index 00000000000..0a8e7a2884d --- /dev/null +++ b/tests/test_util/test_util_inspect_py314.py @@ -0,0 +1,27 @@ +# noqa: I002 as 'from __future__ import annotations' prevents Python 3.14 +# forward references from causing issues, so we must skip it here. + +import sys + +import pytest + +from sphinx.util import inspect + +if sys.version_info[0:2] < (3, 14): + pytest.skip('These tests are for Python 3.14+', allow_module_level=True) + + +def forward_reference_in_args(x: Foo) -> None: # type: ignore[name-defined] # noqa: F821 + pass + + +def forward_reference_in_return() -> Foo: # type: ignore[name-defined] # noqa: F821 + pass + + +def test_signature_forwardref() -> None: + sig = inspect.stringify_signature(inspect.signature(forward_reference_in_args)) + assert sig == '(x: Foo) -> None' + + sig = inspect.stringify_signature(inspect.signature(forward_reference_in_return)) + assert sig == '() -> Foo'