mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2026-10-12 16:32:10 -05:00
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:
1 parent
113cfe1611
commit
dbb4e99d43
8 files changed
+134
-3
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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',
|
||||||
|
'',
|
||||||
|
]
|
||||||
@@ -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'
|
||||||
Reference in new issue
Block a user