From e426f0749430d06f9e824bb0edc883ace5baa79a Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sat, 7 Feb 2026 17:51:04 -0800 Subject: [PATCH 1/4] Fix fixture discovery to use definition order instead of alphabetical Fixtures are now discovered in their definition order (as they appear in source code) rather than alphabetical order. This change resolves issues where fixtures with the same name at different scopes would be processed in unexpected order due to dir() sorting. The fix changes parsefactories() to use __dict__ iteration (which preserves insertion order in Python 3.7+) instead of dir() which sorts. This makes fixture behavior predictable based on source code order. Fixes #11281 Related to #12952 --- changelog/11281.bugfix.rst | 6 ++++ src/_pytest/fixtures.py | 73 +++++++++++++++++++++++--------------- testing/python/fixtures.py | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 changelog/11281.bugfix.rst 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..c84797a4f64 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1869,35 +1869,50 @@ 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). + dicts = [getattr(holderobj, "__dict__", {})] + if safe_isclass(holderobj): + for basecls in holderobj.__mro__: + dicts.append(basecls.__dict__) + + 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 7122f7fef3b..bd86f143754 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5426,3 +5426,67 @@ def test_foobar(self, fixture_bar): ) result = pytester.runpytest("-v") result.assert_outcomes(passed=1) + + +def test_autouse_fixtures_with_same_name_definition_order(pytester: Pytester) -> None: + """ + Test that fixtures with the same name are processed in definition order, + not alphabetical order. The fixture discovered first shadows later 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 setup(): + call_order.append("MODULE") + + class TestFoo: + @classmethod + @pytest.fixture(scope='class', autouse=True) + def setup(cls): + call_order.append("CLASS") + + def test_in_class(self): + # The module fixture is defined first in source order, + # so it shadows the class fixture. Only module fixture runs. + assert call_order == ["MODULE"] + + def test_module_level(): + # Only module fixture runs + assert call_order == ["MODULE"] + """ + ) + result = pytester.runpytest("-v") + result.assert_outcomes(passed=2) + + +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) From f95e9f6e572fbc7db108ab213944ba77b0aaa958 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:51:42 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bd86f143754..f79bc717a0d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5465,10 +5465,12 @@ def test_module_level(): result.assert_outcomes(passed=2) -def test_fixture_override_with_name_kwarg_respects_definition_order(pytester: Pytester) -> None: +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( From d018ef4c2e516e17751ecce1b08741b240b0de30 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Wed, 11 Feb 2026 18:53:47 -0800 Subject: [PATCH 3/4] Fix __dict__ traversal for instances and classes in parsefactories The previous implementation only looked at holderobj.__dict__ which is empty for new instances (e.g. unittest TestCase instances passed from unittest.py). This caused autouse fixtures defined on the class to not be discovered. Now properly handle three cases: - Modules: use module.__dict__ directly - Classes: walk __mro__ to get class and base class attributes - Instances: walk type(instance).__mro__ for class hierarchy Also add assert isinstance(holderobj, type) to satisfy mypy since safe_isclass() doesn't use TypeGuard. --- src/_pytest/fixtures.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c84797a4f64..2a44d84dfc3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1873,10 +1873,19 @@ def parsefactories( # 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). - dicts = [getattr(holderobj, "__dict__", {})] - if safe_isclass(holderobj): - for basecls in holderobj.__mro__: - dicts.append(basecls.__dict__) + # + # 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. + 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: From 8fad34760b5fe211b737f1c40dc4c83710c48a36 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Wed, 11 Feb 2026 19:13:41 -0800 Subject: [PATCH 4/4] Fix mypy errors and correct test expectations - Add explicit type annotation `list[Mapping[str, Any]]` for dicts variable to fix mypy error about MappingProxyType vs dict mismatch (cls.__dict__ returns MappingProxyType for classes) - Fix test_autouse_fixtures_definition_order_preserved to use distinct fixture names and correct expectations (module + class fixtures both run, not just module) --- src/_pytest/fixtures.py | 1 + testing/python/fixtures.py | 24 ++++++++++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 2a44d84dfc3..2024fe678a6 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1878,6 +1878,7 @@ def parsefactories( # 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): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index f79bc717a0d..031dc6609c4 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5428,10 +5428,12 @@ def test_foobar(self, fixture_bar): result.assert_outcomes(passed=1) -def test_autouse_fixtures_with_same_name_definition_order(pytester: Pytester) -> None: +def test_autouse_fixtures_definition_order_preserved(pytester: Pytester) -> None: """ - Test that fixtures with the same name are processed in definition order, - not alphabetical order. The fixture discovered first shadows later ones. + 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 """ @@ -5442,27 +5444,21 @@ def test_autouse_fixtures_with_same_name_definition_order(pytester: Pytester) -> call_order = [] @pytest.fixture(scope='module', autouse=True) - def setup(): + def module_setup(): call_order.append("MODULE") class TestFoo: - @classmethod @pytest.fixture(scope='class', autouse=True) - def setup(cls): + def class_setup(self): call_order.append("CLASS") def test_in_class(self): - # The module fixture is defined first in source order, - # so it shadows the class fixture. Only module fixture runs. - assert call_order == ["MODULE"] - - def test_module_level(): - # Only module fixture runs - assert call_order == ["MODULE"] + # Module-scoped fixture runs first, then class-scoped. + assert call_order == ["MODULE", "CLASS"] """ ) result = pytester.runpytest("-v") - result.assert_outcomes(passed=2) + result.assert_outcomes(passed=1) def test_fixture_override_with_name_kwarg_respects_definition_order(