Skip to content

Comments

Fix fixture discovery to use definition order instead of alphabetical#14169

Open
veeceey wants to merge 5 commits intopytest-dev:mainfrom
veeceey:fix/issue-11281-autouse-fixtures-order
Open

Fix fixture discovery to use definition order instead of alphabetical#14169
veeceey wants to merge 5 commits intopytest-dev:mainfrom
veeceey:fix/issue-11281-autouse-fixtures-order

Conversation

@veeceey
Copy link

@veeceey veeceey commented Feb 8, 2026

Summary

Fixes #11281

This PR changes fixture discovery to preserve definition order instead of using alphabetical sorting from dir().

Problem

Previously, parsefactories() used dir() to iterate over object attributes, which sorts names alphabetically. This caused unexpected behavior when fixtures with the same name were defined at different scopes (e.g., module vs class level), because the order of discovery depended on alphabetical sorting rather than source code order.

Solution

Changed parsefactories() to use __dict__ iteration (similar to how PyCollector.collect() works), which preserves insertion/definition order in Python 3.7+. This ensures fixtures are processed in the order they appear in source code, making behavior predictable and intuitive.

Changes

  • Modified FixtureManager.parsefactories() to iterate __dict__ instead of dir()
  • Added logic to handle MRO for class fixtures (similar to PyCollector)
  • Added tests to verify definition order is respected
  • Added changelog entry

Test plan

@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Feb 8, 2026
@veeceey
Copy link
Author

veeceey commented Feb 19, 2026

Hi maintainers, friendly ping on this PR. It's been open for about 10 days. I see the merge state is showing a conflict -- I'll rebase to resolve that. In the meantime, would appreciate any feedback on the approach (using __dict__ iteration instead of dir() to preserve fixture definition order). Thank you!

veeceey and others added 4 commits February 19, 2026 22:32
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 pytest-dev#11281
Related to pytest-dev#12952
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.
- 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)
@veeceey veeceey force-pushed the fix/issue-11281-autouse-fixtures-order branch from 8601e0f to 8fad347 Compare February 20, 2026 06:33
The elif safe_isclass(holderobj) branch was unreachable in practice
since parsefactories() is never called with a class directly (only
modules and instances). Merged it with the instance branch by using
holderobj_tp, which already resolves to the correct type for both
classes and instances (set on line 1866-1869).
Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @veeceey, please see my comments.

Comment on lines +1873 to +1896
# 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/instances: walk the MRO to get all class and base class
# attributes (using holderobj_tp which resolves to the class in both cases).
dicts: list[Mapping[str, Any]]
if isinstance(holderobj, types.ModuleType):
dicts = [holderobj.__dict__]
else:
# For classes and instances: walk the MRO to get all attributes.
# For classes, holderobj_tp is the class itself; for instances,
# holderobj_tp is type(holderobj) (set above on line 1867).
# In both cases holderobj_tp is a type with __mro__.
assert isinstance(holderobj_tp, type)
dicts = [cls.__dict__ for cls in holderobj_tp.__mro__]

seen: set[str] = set()
for dic in dicts:
for name in dic:
if name in seen:
continue
seen.add(name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please extract this logic into its own function? That would make it easier to understand and allow to add a unit test to ensure the desired behavior in isolation.

result.assert_outcomes(passed=1)


def test_autouse_fixtures_definition_order_preserved(pytester: Pytester) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 tests pass for me on main, so they are not really testing the underlying issue. Could you please review them?

holderobj_tp = holderobj

self._holderobjseen.add(holderobj)
for name in dir(holderobj):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the fix is correct: when discovering the fixtures for a module, we holderobj will be a Module. We then find a test class, we will collect it in a later call, where holderobj will be the class itself.

So I don't think the order returned by dir is not related to the original issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Autouse fixtures with the same name in class and module scopes are executed in the wrong order

2 participants