Skip to content
Open
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 @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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

Expand Down
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 @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions tests/test_util/test_util_inspect_py314.py
Original file line number Diff line number Diff line change
@@ -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'
Loading