Skip to content

Commit 242522a

Browse files
Defer annotation eval on Python 3.14 (#13550) (#13553)
Fixes #13549 (cherry picked from commit 8ca783c) Co-authored-by: Marc Mueller <[email protected]>
1 parent 5bc2f47 commit 242522a

File tree

5 files changed

+59
-4
lines changed

5 files changed

+59
-4
lines changed

changelog/13549.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
No longer evaluate type annotations in Python ``3.14`` when inspecting function signatures.
2+
3+
This prevents crashes during module collection when modules do not explicitly use ``from __future__ import annotations`` and import types for annotations within a ``if TYPE_CHECKING:`` block.

src/_pytest/compat.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import functools
99
import inspect
1010
from inspect import Parameter
11-
from inspect import signature
11+
from inspect import Signature
1212
import os
1313
from pathlib import Path
1414
import sys
@@ -19,6 +19,10 @@
1919
import py
2020

2121

22+
if sys.version_info >= (3, 14):
23+
from annotationlib import Format
24+
25+
2226
#: constant to prepare valuing pylib path replacements/lazy proxies later on
2327
# intended for removal in pytest 8.0 or 9.0
2428

@@ -60,6 +64,13 @@ def is_async_function(func: object) -> bool:
6064
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
6165

6266

67+
def signature(obj: Callable[..., Any]) -> Signature:
68+
"""Return signature without evaluating annotations."""
69+
if sys.version_info >= (3, 14):
70+
return inspect.signature(obj, annotation_format=Format.STRING)
71+
return inspect.signature(obj)
72+
73+
6374
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
6475
function = get_real_func(function)
6576
fn = Path(inspect.getfile(function))

src/_pytest/fixtures.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from _pytest.compat import NotSetType
5050
from _pytest.compat import safe_getattr
5151
from _pytest.compat import safe_isclass
52+
from _pytest.compat import signature
5253
from _pytest.config import _PluggyPlugin
5354
from _pytest.config import Config
5455
from _pytest.config import ExitCode
@@ -804,8 +805,8 @@ def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str:
804805
path, lineno = getfslineno(factory)
805806
if isinstance(path, Path):
806807
path = bestrelpath(self._pyfuncitem.session.path, path)
807-
signature = inspect.signature(factory)
808-
return f"{path}:{lineno + 1}: def {factory.__name__}{signature}"
808+
sig = signature(factory)
809+
return f"{path}:{lineno + 1}: def {factory.__name__}{sig}"
809810

810811
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
811812
self._fixturedef.addfinalizer(finalizer)

src/_pytest/nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from collections.abc import MutableMapping
99
from functools import cached_property
1010
from functools import lru_cache
11-
from inspect import signature
1211
import os
1312
import pathlib
1413
from pathlib import Path
@@ -29,6 +28,7 @@
2928
from _pytest._code.code import Traceback
3029
from _pytest._code.code import TracebackStyle
3130
from _pytest.compat import LEGACY_PATH
31+
from _pytest.compat import signature
3232
from _pytest.config import Config
3333
from _pytest.config import ConftestImportFailure
3434
from _pytest.config.compat import _check_path

testing/test_collection.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,3 +1895,43 @@ def test_with_yield():
18951895
)
18961896
# Assert that no tests were collected
18971897
result.stdout.fnmatch_lines(["*collected 0 items*"])
1898+
1899+
1900+
def test_annotations_deferred_future(pytester: Pytester):
1901+
"""Ensure stringified annotations don't raise any errors."""
1902+
pytester.makepyfile(
1903+
"""
1904+
from __future__ import annotations
1905+
import pytest
1906+
1907+
@pytest.fixture
1908+
def func() -> X: ... # X is undefined
1909+
1910+
def test_func():
1911+
assert True
1912+
"""
1913+
)
1914+
result = pytester.runpytest()
1915+
assert result.ret == 0
1916+
result.stdout.fnmatch_lines(["*1 passed*"])
1917+
1918+
1919+
@pytest.mark.skipif(
1920+
sys.version_info < (3, 14), reason="Annotations are only skipped on 3.14+"
1921+
)
1922+
def test_annotations_deferred_314(pytester: Pytester):
1923+
"""Ensure annotation eval is deferred."""
1924+
pytester.makepyfile(
1925+
"""
1926+
import pytest
1927+
1928+
@pytest.fixture
1929+
def func() -> X: ... # X is undefined
1930+
1931+
def test_func():
1932+
assert True
1933+
"""
1934+
)
1935+
result = pytester.runpytest()
1936+
assert result.ret == 0
1937+
result.stdout.fnmatch_lines(["*1 passed*"])

0 commit comments

Comments
 (0)