Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ Bugs fixed
* #12797: Fix ``Some type variables (...) are not listed in Generic[...]``
TypeError when inheriting from both Generic and autodoc mocked class.
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
Expand Down
20 changes: 17 additions & 3 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
subject: _SignatureType,
bound_method: bool = False,
Expand All @@ -726,12 +736,16 @@ def signature(

try:
if _should_unwrap(subject):
signature = inspect.signature(subject) # type: ignore[arg-type]
signature = inspect.signature(subject, **signature_kwds) # 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,
**signature_kwds,
)
except ValueError:
# 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())
return_annotation = signature.return_annotation

Expand Down
11 changes: 11 additions & 0 deletions tests/roots/test-ext-autodoc/target/properties.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import sys

TYPE_CHECKING = False
if sys.version_info[:2] < (3, 14) or TYPE_CHECKING:
TypeCheckingOnlyName = int


class Foo:
"""docstring"""

Expand All @@ -20,3 +27,7 @@ def prop1_with_type_comment(self):
def prop2_with_type_comment(cls):
# type: () -> int
"""docstring"""

@property
def prop3_with_undefined_anotation(self) -> TypeCheckingOnlyName:
"""docstring"""
13 changes: 13 additions & 0 deletions tests/test_ext_autodoc/test_ext_autodoc_autoclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

import sys
import typing

import pytest
Expand Down Expand Up @@ -208,6 +209,11 @@ def test_decorators() -> 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}
actual = do_autodoc('class', 'target.properties.Foo', options=options)
assert actual == [
Expand Down Expand Up @@ -247,6 +253,13 @@ def test_properties() -> None:
'',
' docstring',
'',
'',
' .. py:property:: Foo.prop3_with_undefined_anotation',
' :module: target.properties',
f' :type: {type_checking_only_name}',
'',
' docstring',
'',
]


Expand Down
22 changes: 22 additions & 0 deletions tests/test_ext_autodoc/test_ext_autodoc_autoproperty.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from __future__ import annotations

import sys

import pytest

from tests.test_ext_autodoc.autodoc_util import do_autodoc
Expand Down Expand Up @@ -87,3 +89,23 @@ def test_cached_properties_with_type_comment() -> None:
' :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 changes: 15 additions & 0 deletions tests/test_util/test_util_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -172,6 +180,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):
Expand Down
52 changes: 52 additions & 0 deletions tests/test_util/test_util_inspect_py314.py
Original file line number Diff line number Diff line change
@@ -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'
Loading