Use the `FORWARDREF annotation format with inspect.signature()` (#13946)

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
Rui PinheiroandAdam Turner authored and GitHub committed 2025-11-29 01:24:41 +00:00
1 parent 113cfe1611
commit dbb4e99d43
8 files changed
+134 -3

No files matched your search

+1
View File
@@ -102,6 +102,7 @@ Contributors
* Pauli Virtanen -- autodoc improvements, autosummary extension * Pauli Virtanen -- autodoc improvements, autosummary extension
* Rafael Fontenelle -- internationalisation * Rafael Fontenelle -- internationalisation
* \A. Rafey Khan -- improved intersphinx typing * \A. Rafey Khan -- improved intersphinx typing
* Rui Pinheiro -- Python 3.14 forward references support
* Roland Meister -- epub builder * Roland Meister -- epub builder
* Sebastian Wiesner -- image handling, distutils support * Sebastian Wiesner -- image handling, distutils support
* Slawek Figiel -- additional warning suppression * Slawek Figiel -- additional warning suppression
+3
View File
@@ -174,6 +174,9 @@ Bugs fixed
* #12797: Fix ``Some type variables (...) are not listed in Generic[...]`` * #12797: Fix ``Some type variables (...) are not listed in Generic[...]``
TypeError when inheriting from both Generic and autodoc mocked class. TypeError when inheriting from both Generic and autodoc mocked class.
Patch by Ikor Jefocur and Daniel Sperber. Patch by Ikor Jefocur and Daniel Sperber.
* #13945: autodoc: Fix handling of undefined names in annotations by using
the ``FORWARDREF`` :mod:`annotationlib` format.
Patch by Rui Pinheiro and Adam Turner.
Testing Testing
+17 -3
View File
@@ -710,6 +710,16 @@ def _should_unwrap(subject: _SignatureType) -> bool:
) )
# Python 3.14 uses deferred evaluation of annotations by default.
# Using annotationlib's FORWARDREF format gives us more robust handling
# of forward references in type annotations.
signature_kwds: dict[str, Any] = {}
if sys.version_info[:2] >= (3, 14):
import annotationlib # type: ignore[import-not-found]
signature_kwds['annotation_format'] = annotationlib.Format.FORWARDREF
def signature( def signature(
subject: _SignatureType, subject: _SignatureType,
bound_method: bool = False, bound_method: bool = False,
@@ -726,12 +736,16 @@ def signature(
try: try:
if _should_unwrap(subject): if _should_unwrap(subject):
signature = inspect.signature(subject) # type: ignore[arg-type] signature = inspect.signature(subject, **signature_kwds) # type: ignore[arg-type]
else: else:
signature = inspect.signature(subject, follow_wrapped=True) # type: ignore[arg-type] signature = inspect.signature(
subject, # type: ignore[arg-type]
follow_wrapped=True,
**signature_kwds,
)
except ValueError: except ValueError:
# follow built-in wrappers up (ex. functools.lru_cache) # follow built-in wrappers up (ex. functools.lru_cache)
signature = inspect.signature(subject) # type: ignore[arg-type] signature = inspect.signature(subject, **signature_kwds) # type: ignore[arg-type]
parameters = list(signature.parameters.values()) parameters = list(signature.parameters.values())
return_annotation = signature.return_annotation return_annotation = signature.return_annotation
@@ -1,3 +1,10 @@
import sys
TYPE_CHECKING = False
if sys.version_info[:2] < (3, 14) or TYPE_CHECKING:
TypeCheckingOnlyName = int
class Foo: class Foo:
"""docstring""" """docstring"""
@@ -20,3 +27,7 @@ class Foo:
def prop2_with_type_comment(cls): def prop2_with_type_comment(cls):
# type: () -> int # type: () -> int
"""docstring""" """docstring"""
@property
def prop3_with_undefined_anotation(self) -> TypeCheckingOnlyName:
"""docstring"""
@@ -6,6 +6,7 @@ source file translated by test_build.
from __future__ import annotations from __future__ import annotations
import sys
import typing import typing
import pytest import pytest
@@ -208,6 +209,11 @@ def test_decorators() -> None:
def test_properties() -> None: def test_properties() -> None:
if sys.version_info[:2] >= (3, 14):
type_checking_only_name = 'TypeCheckingOnlyName'
else:
type_checking_only_name = 'int'
options = {'members': None} options = {'members': None}
actual = do_autodoc('class', 'target.properties.Foo', options=options) actual = do_autodoc('class', 'target.properties.Foo', options=options)
assert actual == [ assert actual == [
@@ -247,6 +253,13 @@ def test_properties() -> None:
'', '',
' docstring', ' docstring',
'', '',
'',
' .. py:property:: Foo.prop3_with_undefined_anotation',
' :module: target.properties',
f' :type: {type_checking_only_name}',
'',
' docstring',
'',
] ]
@@ -6,6 +6,8 @@ source file translated by test_build.
from __future__ import annotations from __future__ import annotations
import sys
import pytest import pytest
from tests.test_ext_autodoc.autodoc_util import do_autodoc from tests.test_ext_autodoc.autodoc_util import do_autodoc
@@ -87,3 +89,23 @@ def test_cached_properties_with_type_comment() -> None:
' :type: int', ' :type: int',
'', '',
] ]
def test_property_with_undefined_annotation() -> None:
if sys.version_info[:2] >= (3, 14):
type_checking_only_name = 'TypeCheckingOnlyName'
else:
type_checking_only_name = 'int'
actual = do_autodoc(
'property', 'target.properties.Foo.prop3_with_undefined_anotation'
)
assert actual == [
'',
'.. py:property:: Foo.prop3_with_undefined_anotation',
' :module: target.properties',
f' :type: {type_checking_only_name}',
'',
' docstring',
'',
]
+15
View File
@@ -106,6 +106,14 @@ def _decorator(f):
return 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(): def test_TypeAliasForwardRef():
alias = TypeAliasForwardRef('example') alias = TypeAliasForwardRef('example')
sig_str = stringify_annotation(alias, 'fully-qualified-except-typing') sig_str = stringify_annotation(alias, 'fully-qualified-except-typing')
@@ -172,6 +180,13 @@ def test_signature() -> None:
sig = inspect.stringify_signature(inspect.signature(func)) sig = inspect.stringify_signature(inspect.signature(func))
assert sig == '(a, b, c=1, d=2, *e, **f)' 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 test_signature_partial() -> None:
def fun(a, b, c=1, d=2): def fun(a, b, c=1, d=2):
@@ -0,0 +1,52 @@
# noqa: I002 as 'from __future__ import annotations' prevents Python 3.14
# forward references from causing issues, so we must skip it here.
import pytest
from sphinx.util import inspect
from sphinx.util.typing import stringify_annotation
try:
from annotationlib import ForwardRef # type: ignore[import-not-found]
except ImportError:
pytest.skip('Requires annotationlib (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_in_args() -> None:
sig = inspect.signature(forward_reference_in_args)
assert sig.return_annotation is None
assert len(sig.parameters)
assert 'x' in sig.parameters
param = sig.parameters['x']
ann = param.annotation
assert isinstance(ann, ForwardRef)
assert ann.__arg__ == 'Foo'
assert ann.__forward_is_class__ is False
assert ann.__forward_module__ is None
assert ann.__owner__ is not None
assert stringify_annotation(ann) == 'Foo'
def test_signature_forwardref_in_return() -> None:
sig = inspect.signature(forward_reference_in_return)
assert sig.parameters == {}
ann = sig.return_annotation
assert isinstance(ann, ForwardRef)
assert ann.__arg__ == 'Foo'
assert ann.__forward_is_class__ is False
assert ann.__forward_module__ is None
assert ann.__owner__ is not None
assert stringify_annotation(ann) == 'Foo'