diff --git a/changelog/11281.bugfix.rst b/changelog/11281.bugfix.rst new file mode 100644 index 00000000000..e713087e0f1 --- /dev/null +++ b/changelog/11281.bugfix.rst @@ -0,0 +1,6 @@ +Fixed fixture discovery to preserve definition order instead of using alphabetical sorting. + +This ensures that fixtures are processed in the order they appear in source code, +which is important for autouse fixtures and fixture overriding with the ``name`` parameter. +Previously, using ``dir()`` for fixture discovery caused alphabetical sorting, leading to +unexpected behavior when fixtures with the same name were defined at different scopes. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 84f90f946be..2024fe678a6 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1869,35 +1869,60 @@ def parsefactories( holderobj_tp = holderobj self._holderobjseen.add(holderobj) - for name in dir(holderobj): - # The attribute can be an arbitrary descriptor, so the attribute - # access below can raise. safe_getattr() ignores such exceptions. - obj_ub = safe_getattr(holderobj_tp, name, None) - if type(obj_ub) is FixtureFunctionDefinition: - marker = obj_ub._fixture_function_marker - if marker.name: - fixture_name = marker.name - else: - fixture_name = name - - # OK we know it is a fixture -- now safe to look up on the _instance_. - try: - obj = getattr(holderobj, name) - # if the fixture is named in the decorator we cannot find it in the module - except AttributeError: - obj = obj_ub - - func = obj._get_wrapped_function() - - self._register_fixture( - name=fixture_name, - nodeid=nodeid, - func=func, - scope=marker.scope, - params=marker.params, - ids=marker.ids, - autouse=marker.autouse, - ) + + # Use __dict__ to preserve definition order instead of dir() which sorts. + # This ensures fixtures are processed in their definition order, which is + # important for autouse fixtures and fixture overriding (#11281, #12952). + # + # For modules: module.__dict__ contains all module-level names. + # For classes: walk the MRO to get all class and base class attributes. + # For instances (e.g. unittest TestCase instances): walk the class MRO, + # since fixtures are defined on the class, not the instance. + dicts: list[Mapping[str, Any]] + if isinstance(holderobj, types.ModuleType): + dicts = [holderobj.__dict__] + elif safe_isclass(holderobj): + assert isinstance(holderobj, type) + dicts = [cls.__dict__ for cls in holderobj.__mro__] + else: + # Instance: walk the class hierarchy. + dicts = [cls.__dict__ for cls in type(holderobj).__mro__] + + seen: set[str] = set() + for dic in dicts: + for name in dic: + if name in seen: + continue + seen.add(name) + + # The attribute can be an arbitrary descriptor, so the attribute + # access below can raise. safe_getattr() ignores such exceptions. + obj_ub = safe_getattr(holderobj_tp, name, None) + if type(obj_ub) is FixtureFunctionDefinition: + marker = obj_ub._fixture_function_marker + if marker.name: + fixture_name = marker.name + else: + fixture_name = name + + # OK we know it is a fixture -- now safe to look up on the _instance_. + try: + obj = getattr(holderobj, name) + # if the fixture is named in the decorator we cannot find it in the module + except AttributeError: + obj = obj_ub + + func = obj._get_wrapped_function() + + self._register_fixture( + name=fixture_name, + nodeid=nodeid, + func=func, + scope=marker.scope, + params=marker.params, + ids=marker.ids, + autouse=marker.autouse, + ) def getfixturedefs( self, argname: str, node: nodes.Node diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8b9e3fbb0a5..e3ad79b2556 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5399,3 +5399,65 @@ def test_it(request, fix1): ) result = pytester.runpytest("-v") result.assert_outcomes(passed=1) + + +def test_autouse_fixtures_definition_order_preserved(pytester: Pytester) -> None: + """ + Test that fixture discovery uses definition order instead of alphabetical + sorting from dir(). When fixtures have different names, they should be + discovered and registered in their definition order, which ensures + higher-scoped fixtures execute before lower-scoped ones. + + Regression test for https://github.com/pytest-dev/pytest/issues/11281 + """ + pytester.makepyfile( + """ + import pytest + + call_order = [] + + @pytest.fixture(scope='module', autouse=True) + def module_setup(): + call_order.append("MODULE") + + class TestFoo: + @pytest.fixture(scope='class', autouse=True) + def class_setup(self): + call_order.append("CLASS") + + def test_in_class(self): + # Module-scoped fixture runs first, then class-scoped. + assert call_order == ["MODULE", "CLASS"] + """ + ) + result = pytester.runpytest("-v") + result.assert_outcomes(passed=1) + + +def test_fixture_override_with_name_kwarg_respects_definition_order( + pytester: Pytester, +) -> None: + """ + Test that fixture override using name kwarg respects definition order. + + Related to https://github.com/pytest-dev/pytest/issues/12952 + """ + pytester.makepyfile( + """ + import pytest + + @pytest.fixture() + def f1(): + return 1 + + @pytest.fixture(name="f1") + def f2(): + return 2 + + def test_override(f1): + # Later definition should override + assert f1 == 2 + """ + ) + result = pytester.runpytest("-v") + result.assert_outcomes(passed=1)