Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/11281.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
83 changes: 54 additions & 29 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)