From 9584b00675c81779141019b53a95d9a0e8a1515b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:06:11 +0200 Subject: [PATCH 1/3] Defer annotation eval on Python 3.14 --- changelog/13549.bugfix.rst | 1 + src/_pytest/compat.py | 13 ++++++++++++- src/_pytest/fixtures.py | 5 +++-- src/_pytest/nodes.py | 2 +- testing/test_collection.py | 40 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 changelog/13549.bugfix.rst diff --git a/changelog/13549.bugfix.rst b/changelog/13549.bugfix.rst new file mode 100644 index 00000000000..440185cedc7 --- /dev/null +++ b/changelog/13549.bugfix.rst @@ -0,0 +1 @@ +Pytest no longer evaluates type annotations on Python 3.14 when inspecting a function signature. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 7d71838be51..bef8c317bb9 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -8,7 +8,7 @@ import functools import inspect from inspect import Parameter -from inspect import signature +from inspect import Signature import os from pathlib import Path import sys @@ -19,6 +19,10 @@ import py +if sys.version_info >= (3, 14): + from annotationlib import Format + + #: constant to prepare valuing pylib path replacements/lazy proxies later on # intended for removal in pytest 8.0 or 9.0 @@ -60,6 +64,13 @@ def is_async_function(func: object) -> bool: return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) +def signature(obj: Callable[..., Any]) -> Signature: + """Return signature without evaluating annotations.""" + if sys.version_info >= (3, 14): + return inspect.signature(obj, annotation_format=Format.STRING) + return inspect.signature(obj) + + def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: function = get_real_func(function) fn = Path(inspect.getfile(function)) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9966e3414c8..bc5805aaea9 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -49,6 +49,7 @@ from _pytest.compat import NotSetType from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass +from _pytest.compat import signature from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -804,8 +805,8 @@ def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: path, lineno = getfslineno(factory) if isinstance(path, Path): path = bestrelpath(self._pyfuncitem.session.path, path) - signature = inspect.signature(factory) - return f"{path}:{lineno + 1}: def {factory.__name__}{signature}" + sig = signature(factory) + return f"{path}:{lineno + 1}: def {factory.__name__}{sig}" def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6d39de95f5b..6690f6ab1f8 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -8,7 +8,6 @@ from collections.abc import MutableMapping from functools import cached_property from functools import lru_cache -from inspect import signature import os import pathlib from pathlib import Path @@ -29,6 +28,7 @@ from _pytest._code.code import Traceback from _pytest._code.code import TracebackStyle from _pytest.compat import LEGACY_PATH +from _pytest.compat import signature from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config.compat import _check_path diff --git a/testing/test_collection.py b/testing/test_collection.py index dfe10a65220..a8bff2847ba 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1895,3 +1895,43 @@ def test_with_yield(): ) # Assert that no tests were collected result.stdout.fnmatch_lines(["*collected 0 items*"]) + + +def test_annotations_deferred_future(pytester: Pytester): + """Ensure stringified annotations don't raise any errors.""" + pytester.makepyfile( + """ + from __future__ import annotations + import pytest + + @pytest.fixture + def func() -> X: ... # X is undefined + + def test_func(): + assert True + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed*"]) + + +@pytest.mark.skipif( + sys.version_info < (3, 14), reason="Annotations are only skipped on 3.14+" +) +def test_annotations_deferred_314(pytester: Pytester): + """Ensure annotation eval is deferred.""" + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def func() -> X: ... # X is undefined + + def test_func(): + assert True + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed*"]) From 710bc481cf0420e1fc51f7d4385862dca31b8aae Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 22 Jun 2025 12:06:33 -0300 Subject: [PATCH 2/3] Update changelog/13549.bugfix.rst --- changelog/13549.bugfix.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog/13549.bugfix.rst b/changelog/13549.bugfix.rst index 440185cedc7..1b1406554c3 100644 --- a/changelog/13549.bugfix.rst +++ b/changelog/13549.bugfix.rst @@ -1 +1,3 @@ -Pytest no longer evaluates type annotations on Python 3.14 when inspecting a function signature. +No longer evaluate type annotations in Python ``3.14`` when inspecting function signatures. + +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. From 90a36457791769720997955c090bd3325f601789 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:06:54 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/13549.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/13549.bugfix.rst b/changelog/13549.bugfix.rst index 1b1406554c3..e69f6a4d6cf 100644 --- a/changelog/13549.bugfix.rst +++ b/changelog/13549.bugfix.rst @@ -1,3 +1,3 @@ -No longer evaluate type annotations in Python ``3.14`` when inspecting function signatures. +No longer evaluate type annotations in Python ``3.14`` when inspecting function signatures. 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.