Skip to content

Commit e6d722c

Browse files
committed
Use FORWARDREF format in inspect.signature calls
Fixes #13945
1 parent ad3f3cc commit e6d722c

File tree

5 files changed

+65
-3
lines changed

5 files changed

+65
-3
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Contributors
100100
* Pauli Virtanen -- autodoc improvements, autosummary extension
101101
* Rafael Fontenelle -- internationalisation
102102
* \A. Rafey Khan -- improved intersphinx typing
103+
* Rui Pinheiro -- Python 3.14 forward references support
103104
* Roland Meister -- epub builder
104105
* Sebastian Wiesner -- image handling, distutils support
105106
* Slawek Figiel -- additional warning suppression

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ Bugs fixed
132132
directly defined in certain cases, depending on autodoc processing
133133
order.
134134
Patch by Jeremy Maitin-Shepard.
135+
* #13945: sphinx.ext.autodoc: Fix handling of Python 3.14 forward references in
136+
annotations by using ```annotationlib.Format.FORWARDREF``.
137+
Patch by Rui Pinheiro.
135138

136139

137140
Testing

sphinx/util/inspect.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ def __call__(self, obj: Any, name: str, default: Any = ..., /) -> Any: ...
7272
isclass = inspect.isclass
7373
ismodule = inspect.ismodule
7474

75+
# Python 3.14 added the annotationlib module to the standard library as well as the
76+
# 'annotation_format' keyword parameter to inspect.signature(), which allows us to handle
77+
# forward references more robustly.
78+
if sys.version_info[0:2] >= (3, 14):
79+
import annotationlib # type: ignore[import-not-found]
80+
81+
inspect_signature_extra: dict[str, Any] = {
82+
'annotation_format': annotationlib.Format.FORWARDREF
83+
}
84+
else:
85+
inspect_signature_extra: dict[str, Any] = {}
86+
7587

7688
def unwrap(obj: Any) -> Any:
7789
"""Get an original object from wrapped object (wrapped functions).
@@ -718,12 +730,16 @@ def signature(
718730

719731
try:
720732
if _should_unwrap(subject):
721-
signature = inspect.signature(subject) # type: ignore[arg-type]
733+
signature = inspect.signature(subject, **inspect_signature_extra) # type: ignore[arg-type]
722734
else:
723-
signature = inspect.signature(subject, follow_wrapped=True) # type: ignore[arg-type]
735+
signature = inspect.signature(
736+
subject, # type: ignore[arg-type]
737+
follow_wrapped=True,
738+
**inspect_signature_extra,
739+
)
724740
except ValueError:
725741
# follow built-in wrappers up (ex. functools.lru_cache)
726-
signature = inspect.signature(subject) # type: ignore[arg-type]
742+
signature = inspect.signature(subject, **inspect_signature_extra) # type: ignore[arg-type]
727743
parameters = list(signature.parameters.values())
728744
return_annotation = signature.return_annotation
729745

tests/test_util/test_util_inspect.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ def wrapper():
106106
return wrapper
107107

108108

109+
def forward_reference_in_args(x: Foo) -> None: # type: ignore[name-defined] # noqa: F821
110+
pass
111+
112+
113+
def forward_reference_in_return() -> Foo: # type: ignore[name-defined] # noqa: F821
114+
pass
115+
116+
109117
def test_TypeAliasForwardRef():
110118
alias = TypeAliasForwardRef('example')
111119
sig_str = stringify_annotation(alias, 'fully-qualified-except-typing')
@@ -164,6 +172,13 @@ def func(a, b, c=1, d=2, *e, **f):
164172
sig = inspect.stringify_signature(inspect.signature(func))
165173
assert sig == '(a, b, c=1, d=2, *e, **f)'
166174

175+
# forward references
176+
sig = inspect.stringify_signature(inspect.signature(forward_reference_in_args))
177+
assert sig == '(x: Foo)'
178+
179+
sig = inspect.stringify_signature(inspect.signature(forward_reference_in_return))
180+
assert sig == '() -> Foo'
181+
167182

168183
def test_signature_partial() -> None:
169184
def fun(a, b, c=1, d=2):
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# noqa: I002 as 'from __future__ import annotations' prevents Python 3.14
2+
# forward references from causing issues, so we must skip it here.
3+
4+
import sys
5+
6+
import pytest
7+
8+
from sphinx.util import inspect
9+
10+
if sys.version_info[0:2] < (3, 14):
11+
pytest.skip('These tests are for Python 3.14+', allow_module_level=True)
12+
13+
14+
def forward_reference_in_args(x: Foo) -> None: # type: ignore[name-defined] # noqa: F821
15+
pass
16+
17+
18+
def forward_reference_in_return() -> Foo: # type: ignore[name-defined] # noqa: F821
19+
pass
20+
21+
22+
def test_signature_forwardref() -> None:
23+
sig = inspect.stringify_signature(inspect.signature(forward_reference_in_args))
24+
assert sig == '(x: Foo) -> None'
25+
26+
sig = inspect.stringify_signature(inspect.signature(forward_reference_in_return))
27+
assert sig == '() -> Foo'

0 commit comments

Comments
 (0)