From 1dbbcebfc968e5c0b9972abb152c47a5da735924 Mon Sep 17 00:00:00 2001 From: okxint Date: Tue, 15 Sep 2026 00:29:00 +0530 Subject: [PATCH] fix(fixtures): clear stale finalizers when fixture setup fails before caching When `pytest_fixture_setup` raises (e.g. because `-Werror` turns a `PytestRemovedIn10Warning` into an error), `FixtureDef.execute()` exits without setting `cached_result`. This leaves whatever was already appended to `self._finalizers` (the `pytest_fixture_post_finalizer` lambda registered just before the call) sitting there permanently, because `finish()` is a no-op when `cached_result is None`. The next test that uses the same `FixtureDef` then calls `execute()` again and hits: assert not self._finalizers with an internal `AssertionError`, masking the real deprecation error. Fix: clear `self._finalizers` at the start of the "fresh execution" path in `execute()`, before registering new finalizers. This is safe because: - If no previous setup ran, the list is already empty. - If a previous setup succeeded, `finish()` already cleared it. - If a previous setup failed (our case), the stale entries are removed so the next execution gets a clean slate. Fixes #14775 --- src/_pytest/fixtures.py | 8 +++++++ testing/deprecated_test.py | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7656fca2f5b..cf916e230d8 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1275,6 +1275,14 @@ def execute(self, request: SubRequest) -> FixtureValue: for parent_fixture in requested_fixtures_that_should_finalize_us: parent_fixture.addfinalizer(finalizer) + # If a previous call to execute() raised before caching the result + # (e.g. because pytest_fixture_setup turned a warning into an error), + # self._finalizers may still contain stale entries: finish() is a + # no-op when cached_result is None (it assumes "already finished"), + # so those finalizers are never drained. Clear them here so this + # fresh execution starts from a clean slate. + # See https://github.com/pytest-dev/pytest/issues/14775 + self._finalizers.clear() # Register the pytest_fixture_post_finalizer as the first finalizer, # which is executed last. assert not self._finalizers diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index ee91880d23a..ae6bf888d90 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -123,6 +123,49 @@ def test_foo(self, fix): ) +def test_class_scoped_instance_method_werror_multiple_tests( + pytester: Pytester, +) -> None: + """Multiple tests sharing a class-scoped instance-method fixture must each report + the deprecation warning (not an internal AssertionError) when -Werror is active. + + Regression test for https://github.com/pytest-dev/pytest/issues/14775. + When -Werror turns the PytestRemovedIn10Warning into an error during fixture + setup, FixtureDef.execute() exited without caching the result, leaving stale + entries in self._finalizers. The second test then hit + ``assert not self._finalizers`` with an internal AssertionError instead of the + expected deprecation error. + """ + 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::pytest.PytestRemovedIn10Warning") + result.assert_outcomes(errors=2) + # Both tests should report the deprecation warning, not an AssertionError. + result.stdout.fnmatch_lines( + [ + "*ERROR at setup of TestFixt.test_1*", + "*PytestRemovedIn10Warning*", + "*ERROR at setup of TestFixt.test_2*", + "*PytestRemovedIn10Warning*", + ] + ) + result.stdout.no_fnmatch_line("*AssertionError*") + + @pytest.mark.parametrize( "scope", [Scope.Class, Scope.Module, Scope.Package, Scope.Session] )