From cc007972478cf82f9bbd4abf4c31d285e175f5df Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Wed, 16 Sep 2026 22:31:00 +0700 Subject: [PATCH 1/6] Cancel autouse fixture shadowed by non-autouse override Co-authored-by: Claude Sonnet 5 --- changelog/3225.bugfix.rst | 1 + src/_pytest/fixtures.py | 19 +++++++++++++++++-- testing/python/fixtures.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 changelog/3225.bugfix.rst diff --git a/changelog/3225.bugfix.rst b/changelog/3225.bugfix.rst new file mode 100644 index 00000000000..b233a312053 --- /dev/null +++ b/changelog/3225.bugfix.rst @@ -0,0 +1 @@ +A non-autouse fixture overriding an autouse one no longer runs automatically. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7656fca2f5b..f5cb8a5395c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1938,14 +1938,29 @@ def pytest_collection_finish(self) -> None: def _getautousenames(self, node: nodes.Node) -> Iterator[str]: """Return the names of autouse fixtures visible to node.""" + usefixtures_ini = set(self.config.getini("usefixtures")) for parentnode in node.listchain(): basenames = self._node_autousenames.get(parentnode) if basenames: - yield from basenames + for name in basenames: + if name in usefixtures_ini or self._is_autouse(name, node): + yield name # Legacy fallback: check string-based nodeid autouse names. nodeid_basenames = self._nodeid_autousenames.get(parentnode.nodeid) if nodeid_basenames: - yield from nodeid_basenames + for name in nodeid_basenames: + if self._is_autouse(name, node): + yield name + + def _is_autouse(self, name: str, node: nodes.Node) -> bool: + """Whether the fixture resolved for name is itself autouse. + + A non-autouse override cancels the autouse fixture it shadows (#3225). + """ + fixturedefs = self.getfixturedefs(name, node) + if not fixturedefs: + return True + return fixturedefs[-1]._autouse def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: """Return the names of usefixtures fixtures visible to node.""" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8e779a93d76..2b779590b22 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2560,6 +2560,39 @@ def test_hello(arg1): reprec = pytester.inline_run() reprec.assertoutcome(passed=1) + def test_autouse_cancelled_by_non_autouse_override( + self, pytester: Pytester + ) -> None: + """A non-autouse override cancels the autouse fixture it shadows (#3225).""" + pytester.makeconftest( + """ + import pytest + + @pytest.fixture(autouse=True) + def foo(): + pass + """ + ) + pytester.makepyfile( + """ + import pytest + + @pytest.fixture() + def foo(): + assert False + + def test_bar(foo): + pass + + def test_baz(): + pass + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1, errors=1) + result = pytester.runpytest("-o", "usefixtures=foo") + result.assert_outcomes(errors=2) + @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) def test_ordering_dependencies_torndown_first( From ead92f37c394beeb7c3bf5852d874130c910005c Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Thu, 17 Sep 2026 13:53:56 +0700 Subject: [PATCH 2/6] Cover class-level override and document _autouse invariance Co-authored-by: Claude Sonnet 5 --- changelog/3225.bugfix.rst | 2 +- src/_pytest/fixtures.py | 4 ++-- testing/python/fixtures.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/changelog/3225.bugfix.rst b/changelog/3225.bugfix.rst index b233a312053..9985da918e7 100644 --- a/changelog/3225.bugfix.rst +++ b/changelog/3225.bugfix.rst @@ -1 +1 @@ -A non-autouse fixture overriding an autouse one no longer runs automatically. +A non-autouse fixture overriding an autouse one no longer runs automatically, without changing explicit fixture requests. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f5cb8a5395c..eb37790f4d0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1124,7 +1124,7 @@ def __init__( ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, *, node: nodes.Node | NotSetType = NOTSET, - # only used in a deprecationwarning msg, can be removed in pytest9 + # Whether the fixture is autouse; consulted during fixture closure (#3225). _autouse: bool = False, _ispytest: bool = False, ) -> None: @@ -1186,7 +1186,7 @@ def __init__( self.cached_result: _FixtureCachedResult[FixtureValue] | None = None self._finalizers: Final[list[Callable[[], object]]] = [] - # only used to emit a deprecationwarning, can be removed in pytest9 + # Whether the fixture is autouse; consulted during fixture closure (#3225). self._autouse = _autouse @property diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 2b779590b22..641c3e64201 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2593,6 +2593,36 @@ def test_baz(): result = pytester.runpytest("-o", "usefixtures=foo") result.assert_outcomes(errors=2) + def test_autouse_cancelled_by_non_autouse_class_override( + self, pytester: Pytester + ) -> None: + """A non-autouse class-level override cancels the autouse fixture (#3225).""" + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(autouse=True) + def foo(): + pass + + class TestClass: + @pytest.fixture() + def foo(self): + assert False + + def test_with_request(self, foo): + pass + + def test_no_request(self): + pass + + def test_module_level(): + pass + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=2, errors=1) + @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) def test_ordering_dependencies_torndown_first( From bd1b30d139db19de95825a730ecb37ccbb59968f Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Thu, 17 Sep 2026 14:36:43 +0700 Subject: [PATCH 3/6] Restore nested loops and pragma the legacy branch --- src/_pytest/fixtures.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index eb37790f4d0..61bf2e09536 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1945,9 +1945,9 @@ def _getautousenames(self, node: nodes.Node) -> Iterator[str]: for name in basenames: if name in usefixtures_ini or self._is_autouse(name, node): yield name - # Legacy fallback: check string-based nodeid autouse names. + # Legacy fallback: string-based nodeid autouse names. nodeid_basenames = self._nodeid_autousenames.get(parentnode.nodeid) - if nodeid_basenames: + if nodeid_basenames: # pragma: no cover for name in nodeid_basenames: if self._is_autouse(name, node): yield name @@ -1958,9 +1958,7 @@ def _is_autouse(self, name: str, node: nodes.Node) -> bool: A non-autouse override cancels the autouse fixture it shadows (#3225). """ fixturedefs = self.getfixturedefs(name, node) - if not fixturedefs: - return True - return fixturedefs[-1]._autouse + return not fixturedefs or fixturedefs[-1]._autouse def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: """Return the names of usefixtures fixtures visible to node.""" From 5e3d178a2399f6c51fa11f34b929c4becf865219 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Thu, 17 Sep 2026 14:45:21 +0700 Subject: [PATCH 4/6] Remove `# pragma: no cover` --- src/_pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 61bf2e09536..118557e3c76 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1947,7 +1947,7 @@ def _getautousenames(self, node: nodes.Node) -> Iterator[str]: yield name # Legacy fallback: string-based nodeid autouse names. nodeid_basenames = self._nodeid_autousenames.get(parentnode.nodeid) - if nodeid_basenames: # pragma: no cover + if nodeid_basenames: for name in nodeid_basenames: if self._is_autouse(name, node): yield name From 4e0d129d020998b28da6f6835137b7b5bbab6648 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Thu, 17 Sep 2026 15:07:16 +0700 Subject: [PATCH 5/6] Pin legacy nodeid autouse path with in-process test Co-authored-by: Claude Sonnet 5 --- testing/python/fixtures.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 641c3e64201..20cb61bca38 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2623,6 +2623,40 @@ def test_module_level(): result = pytester.runpytest() result.assert_outcomes(passed=2, errors=1) + def test_getautousenames_legacy_nodeid_autouse(self, pytester: Pytester) -> None: + """Autouse registered via the deprecated nodeid API is still yielded.""" + pytester.makeconftest( + """ + import pytest + + @pytest.fixture + def fm(request): + return request._fixturemanager + + @pytest.fixture + def item(request): + return request._pyfuncitem + """ + ) + pytester.makepyfile( + """ + import warnings + + def test_legacy(item, fm): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fm._register_fixture( + name="legacy_auto", + func=lambda: None, + nodeid=item.nodeid, + autouse=True, + ) + assert "legacy_auto" in list(fm._getautousenames(item)) + """ + ) + reprec = pytester.inline_run() + reprec.assertoutcome(passed=1) + @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) def test_ordering_dependencies_torndown_first( From 7b2d648a3fe740d3096363ff98df34269cd28e96 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Thu, 17 Sep 2026 15:34:38 +0700 Subject: [PATCH 6/6] Cover legacy override-cancel branch --- testing/python/fixtures.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 20cb61bca38..9d599216db9 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2652,6 +2652,15 @@ def test_legacy(item, fm): autouse=True, ) assert "legacy_auto" in list(fm._getautousenames(item)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fm._register_fixture( + name="legacy_auto", + func=lambda: None, + nodeid=item.nodeid, + autouse=False, + ) + assert "legacy_auto" not in list(fm._getautousenames(item)) """ ) reprec = pytester.inline_run()