diff --git a/changelog/11277.bugfix.rst b/changelog/11277.bugfix.rst new file mode 100644 index 00000000000..43370561e3b --- /dev/null +++ b/changelog/11277.bugfix.rst @@ -0,0 +1,2 @@ +Fixed a bug that when there are multiple fixtures for an indirect parameter, +the scope of the highest-scope fixture is picked for the parameter set, instead of that of the one with the narrowest scope. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 00c2a8ef432..6e99ec18895 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -492,7 +492,7 @@ def node(self): node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem elif scope is Scope.Package: # FIXME: _fixturedef is not defined on FixtureRequest (this class), - # but on FixtureRequest (a subclass). + # but on SubRequest (a subclass). node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] else: node = get_scope_node(self._pyfuncitem, scope) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c0c16d4d0d8..ae42e390f56 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1516,7 +1516,7 @@ def _find_parametrized_scope( if all_arguments_are_fixtures: fixturedefs = arg2fixturedefs or {} used_scopes = [ - fixturedef[0]._scope + fixturedef[-1]._scope for name, fixturedef in fixturedefs.items() if name in argnames ] @@ -1682,7 +1682,7 @@ class Function(PyobjMixin, nodes.Item): :param config: The pytest Config object. :param callspec: - If given, this is function has been parametrized and the callspec contains + If given, this function has been parametrized and the callspec contains meta information about the parametrization. :param callobj: If given, the object which will be called when the Function is invoked, diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index bb4ae9d9af3..4c066a89d79 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -151,6 +151,7 @@ class DummyFixtureDef: module_fix=[DummyFixtureDef(Scope.Module)], class_fix=[DummyFixtureDef(Scope.Class)], func_fix=[DummyFixtureDef(Scope.Function)], + mixed_fix=[DummyFixtureDef(Scope.Module), DummyFixtureDef(Scope.Class)], ), ) @@ -187,6 +188,7 @@ def find_scope(argnames, indirect): ) == Scope.Module ) + assert find_scope(["mixed_fix"], indirect=True) == Scope.Class def test_parametrize_and_id(self) -> None: def func(x, y): @@ -1503,6 +1505,66 @@ def test_it(x): pass result = pytester.runpytest() assert result.ret == 0 + def test_reordering_with_scopeless_and_just_indirect_parametrization( + self, pytester: Pytester + ) -> None: + pytester.makeconftest( + """ + import pytest + + @pytest.fixture(scope="package") + def fixture1(): + pass + """ + ) + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(scope="module") + def fixture0(): + pass + + @pytest.fixture(scope="module") + def fixture1(fixture0): + pass + + @pytest.mark.parametrize("fixture1", [0], indirect=True) + def test_0(fixture1): + pass + + @pytest.fixture(scope="module") + def fixture(): + pass + + @pytest.mark.parametrize("fixture", [0], indirect=True) + def test_1(fixture): + pass + + def test_2(): + pass + + class Test: + @pytest.fixture(scope="class") + def fixture(self, fixture): + pass + + @pytest.mark.parametrize("fixture", [0], indirect=True) + def test_3(self, fixture): + pass + """ + ) + result = pytester.runpytest("-v") + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + "*test_0*", + "*test_1*", + "*test_2*", + "*test_3*", + ] + ) + class TestMetafuncFunctionalAuto: """Tests related to automatically find out the correct scope for