From b98af3e8034edfa47fc5b832f6249c91a2c2b5fa Mon Sep 17 00:00:00 2001 From: Kyue <164024549+Gooh456@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:53:59 +0100 Subject: [PATCH] Cache fixture setup failures that happen before the fixture function runs If resolve_fixture_function() (or the async-fixture check) raises before call_fixture_func() is reached, pytest_fixture_setup() never wrote a cached_result for the FixtureDef. The exception still propagates and fails the current test correctly, but the FixtureDef is left in a half-registered state: the pytest_fixture_post_finalizer callback added in execute() is never popped, because that only happens via finish(), and finish() for a class/module/session-scoped fixture doesn't run until the owning scope's node tears down. The next test that requests the same fixture instance then hits `assert not self._finalizers` in FixtureDef.execute() and blows up with an internal AssertionError instead of reporting the original problem. This is easy to trigger now that resolving a class-scoped fixture defined as an instance method emits a warning (deprecations.rst), which becomes an exception under -W error before the fixture body ever runs. Move fixturefunc = resolve_fixture_function(...) and the async-fixture check inside the existing try/except TEST_OUTCOME block so any failure during setup, not just failures inside the fixture body, gets recorded in cached_result the same way. Subsequent requests then correctly re-raise the cached failure instead of tripping the internal assert. Fixes #14775 Signed-off-by: Kyue <164024549+Gooh456@users.noreply.github.com> --- AUTHORS | 1 + changelog/14775.bugfix.rst | 1 + src/_pytest/fixtures.py | 25 +++++++++++++------------ testing/python/fixtures.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 changelog/14775.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 229e4315078..2f3db95cb54 100644 --- a/AUTHORS +++ b/AUTHORS @@ -276,6 +276,7 @@ Kojo Idrissa Kostis Anagnostopoulos Kristoffer Nordström Kyle Altendorf +Kyue Lawrence Mitchell Lee Kamentsky Leonardus Chen diff --git a/changelog/14775.bugfix.rst b/changelog/14775.bugfix.rst new file mode 100644 index 00000000000..3aba430f9d9 --- /dev/null +++ b/changelog/14775.bugfix.rst @@ -0,0 +1 @@ +Fixed an internal ``AssertionError`` that could occur when a fixture's setup fails before its underlying function is even called (e.g. when the class-scoped-fixture-as-instance-method deprecation warning is turned into an error via ``-W error``). The failure is now cached like any other setup error instead of leaking finalizer state into the next test. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b516c0a75d1..d7a43e2741f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1320,21 +1320,22 @@ def pytest_fixture_setup( for argname in fixturedef.argnames: kwargs[argname] = request.getfixturevalue(argname) - fixturefunc = resolve_fixture_function(fixturedef, request) my_cache_key = fixturedef.cache_key(request) - if inspect.isasyncgenfunction(fixturefunc) or inspect.iscoroutinefunction( - fixturefunc - ): - auto_str = " with autouse=True" if fixturedef._autouse else "" - fail( - f"{request.node.name!r} requested an async fixture {request.fixturename!r}{auto_str}, " - "with no plugin or hook that handled it. This is an error, as pytest does not natively support it.\n" - "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", - pytrace=False, - ) - try: + fixturefunc = resolve_fixture_function(fixturedef, request) + + if inspect.isasyncgenfunction(fixturefunc) or inspect.iscoroutinefunction( + fixturefunc + ): + auto_str = " with autouse=True" if fixturedef._autouse else "" + fail( + f"{request.node.name!r} requested an async fixture {request.fixturename!r}{auto_str}, " + "with no plugin or hook that handled it. This is an error, as pytest does not natively support it.\n" + "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", + pytrace=False, + ) + result = call_fixture_func(fixturefunc, request, kwargs) except TEST_OUTCOME as e: if isinstance(e, skip.Exception): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 69044226b6d..fdd79b1ade9 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3801,6 +3801,42 @@ def test_3(bad): pass lines2 = failures[2].longrepr.reprtraceback.reprentries[0].lines assert len(lines1) == len(lines2) + def test_exception_before_fixture_func_called_is_cached( + self, pytester: Pytester + ) -> None: + """A failure that happens while resolving the fixture function itself + (e.g. a deprecation warning promoted to an error by ``-W error``, before + the fixture body ever runs) must be cached on the FixtureDef like any + other setup failure. Otherwise leftover finalizer state leaks into the + next test using the same class-scoped fixture and crashes pytest + internals instead of reporting the (repeated) original error (#14775). + """ + pytester.makepyfile( + """ + import pytest + + class TestFixt: + @pytest.fixture(scope="class") + def fixt(self): + yield + + def test_1(self, fixt): + pass + + def test_2(self, fixt): + pass + """ + ) + result = pytester.runpytest("-Werror") + result.assert_outcomes(errors=2) + assert "AssertionError" not in result.stdout.str() + result.stdout.fnmatch_lines( + [ + "*PytestRemovedIn10Warning: Class-scoped fixtures defined as " + "instance methods are deprecated.*", + ] + ) + class TestShowFixtures: def test_funcarg_compat(self, pytester: Pytester) -> None: