From f0d2dedc87f40fa73d606346b1e2457d939ed0d3 Mon Sep 17 00:00:00 2001 From: KirtiRamchandani Date: Sun, 14 Jun 2026 00:16:57 +0000 Subject: [PATCH] Restore precedence of indirect parametrize over fixture params Fixes #14591 Commit d56b1af52 removed a special case in pytest_generate_tests that skipped applying fixture params when the test already parametrizes the same argument via @pytest.mark.parametrize. That removal caused duplicate parametrization errors when overriding a fixture's params with indirect parametrize marks. Restore the precedence check and add a regression test. --- src/_pytest/fixtures.py | 14 ++++++++++++++ testing/python/fixtures.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c60823ed510..f4ca2eac455 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -64,6 +64,7 @@ from _pytest.deprecated import PARSEFACTORIES_NODEID_DEPRECATED 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 @@ -1898,10 +1899,23 @@ 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, ()) + # 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 diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index de3f7764aa7..093f44451d3 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4970,6 +4970,39 @@ def test_indirect(arg2): result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"]) +def test_indirect_parametrize_overrides_fixture_params(pytester: Pytester) -> None: + """Indirect parametrization should override fixture params (#14591).""" + pytester.makepyfile( + """ + import pytest + + class MyFixture: + def __init__(self, mode): + self.mode = mode + + @pytest.fixture(params=['first_mode', 'second_mode']) + def myfixture(request): + yield MyFixture(request.param) + + def test_myfixture_default(myfixture): + assert isinstance(myfixture, MyFixture) + assert myfixture.mode in {'first_mode', 'second_mode'} + + @pytest.mark.parametrize('myfixture', ['first_mode'], indirect=True) + def test_myfixture_single_mode1(myfixture): + assert isinstance(myfixture, MyFixture) + assert myfixture.mode == 'first_mode' + + @pytest.mark.parametrize('myfixture', ['second_mode'], indirect=True) + def test_myfixture_single_mode2(myfixture): + assert isinstance(myfixture, MyFixture) + assert myfixture.mode == 'second_mode' + """ + ) + result = pytester.runpytest("-v") + result.assert_outcomes(passed=4) + + def test_fixture_named_request(pytester: Pytester) -> None: pytester.copy_example("fixtures/test_fixture_named_request.py") result = pytester.runpytest()