From d56b1af5252ceb8578d6751a6b2006e79defbb08 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Nov 2025 12:25:11 +0200 Subject: [PATCH 1/4] fixtures: remove no longer needed special case in `pytest_generate_tests` Originally added in c00fe960baa40b5a4b460988d2d1202a8e3d668b, but since made redundant. The `ignore_args` parameter in `getfixtureclosure()` (populated by `_get_direct_parametrize_args()`) now handles the precedence automatically. When a test directly parametrizes an argument, it's included in `ignore_args`, which prevents the fixture from being included in the closure computation in the first place. --- src/_pytest/fixtures.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 27846db13a4..9b88ba37224 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -56,7 +56,6 @@ from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import YIELD_FIXTURE from _pytest.main import Session -from _pytest.mark import Mark from _pytest.mark import ParameterSet from _pytest.mark.structures import MarkDecorator from _pytest.outcomes import fail @@ -1695,11 +1694,6 @@ def sort_by_scope(arg_name: str) -> Scope: def pytest_generate_tests(self, metafunc: Metafunc) -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" - - def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: - args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs) - return args - for argname in metafunc.fixturenames: # Get the FixtureDefs for the argname. fixture_defs = metafunc._arg2fixturedefs.get(argname) @@ -1708,14 +1702,6 @@ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: # else (e.g @pytest.mark.parametrize) continue - # If the test itself parametrizes using this argname, give it - # precedence. - if any( - argname in get_parametrize_mark_argnames(mark) - for mark in metafunc.definition.iter_markers("parametrize") - ): - continue - # In the common case we only look at the fixture def with the # closest scope (last in the list). But if the fixture overrides # another fixture, while requesting the super fixture, keep going From 934a0b1f1385ff1a5391f307f7890562d4921f69 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Nov 2025 13:35:54 +0200 Subject: [PATCH 2/4] fixtures: remove distracting comment This is incidental to this function, better to focus on what it does. --- src/_pytest/fixtures.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9b88ba37224..4fa29aaa420 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1696,11 +1696,7 @@ def pytest_generate_tests(self, metafunc: Metafunc) -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" for argname in metafunc.fixturenames: # Get the FixtureDefs for the argname. - fixture_defs = metafunc._arg2fixturedefs.get(argname) - if not fixture_defs: - # Will raise FixtureLookupError at setup time if not parametrized somewhere - # else (e.g @pytest.mark.parametrize) - continue + fixture_defs = metafunc._arg2fixturedefs.get(argname, ()) # In the common case we only look at the fixture def with the # closest scope (last in the list). But if the fixture overrides From b1dc3db7614ca8b72126d15dc6f5deddab2e8fc5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Nov 2025 13:48:45 +0200 Subject: [PATCH 3/4] fixtures: add an xfail'd test for overriding parametrizing fixture transitively Currently a known bug, hopefully we can fix it in the future. --- src/_pytest/fixtures.py | 4 ++++ testing/python/fixtures.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4fa29aaa420..5e124d1fe80 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1715,6 +1715,10 @@ def pytest_generate_tests(self, metafunc: Metafunc) -> None: break # Not requesting the overridden super fixture, stop. + # + # TODO: Handle the case where the super-fixture is transitively + # requested (see #7737 and the xfail'd test + # test_override_parametrized_fixture_via_transitive_fixture). if argname not in fixturedef.argnames: break diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6a65dce3c4d..8b9e3fbb0a5 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -590,6 +590,40 @@ def test_spam(foo): result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) + @pytest.mark.xfail(reason="not handled currently") + def test_override_parametrized_fixture_via_transitive_fixture( + self, pytester: Pytester + ) -> None: + """Test that overriding a parametrized fixture works even the super + fixture is requested only transitively. + + Regression test for #7737. + """ + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(params=[1, 2]) + def foo(request): + return request.param + + @pytest.fixture + def bar(foo): + return foo + + class TestIt: + @pytest.fixture + def foo(self, bar): + return bar * 2 + + def test_it(self, foo): + pass + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=2) + def test_autouse_fixture_plugin(self, pytester: Pytester) -> None: # A fixture from a plugin has no baseid set, which screwed up # the autouse fixture handling. From 088718a43779d36bc43c7daa0bdf253803bf41e4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Nov 2025 14:09:12 +0200 Subject: [PATCH 4/4] fixtures: remove an incorrect comment The `request` thing is really accurate. The comment in the docstring is more accurate, so let's remove this redundant comment. --- src/_pytest/fixtures.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5e124d1fe80..701be0283a1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -345,11 +345,6 @@ def prune_dependency_tree(self) -> None: working_set = set(self.initialnames) while working_set: argname = working_set.pop() - # Argname may be something not included in the original names_closure, - # in which case we ignore it. This currently happens with pseudo - # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. - # So they introduce the new dependency 'request' which might have - # been missing in the original tree (closure). if argname not in closure and argname in self.names_closure: closure.add(argname) if argname in self.name2fixturedefs: