Skip to content

fix: iter_markers/get_closest_marker returns correct closest MRO marker (#14329)#14630

Draft
RonnyPfannschmidt wants to merge 1 commit into
pytest-dev:mainfrom
RonnyPfannschmidt:fix/iter-markers-mro-closest-first
Draft

fix: iter_markers/get_closest_marker returns correct closest MRO marker (#14329)#14630
RonnyPfannschmidt wants to merge 1 commit into
pytest-dev:mainfrom
RonnyPfannschmidt:fix/iter-markers-mro-closest-first

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Jun 20, 2026

Copy link
Copy Markdown
Member

Alternative approach to #14332 for fixing #14329 (get_closest_marker returning the wrong marker with class inheritance).

  • Introduces Node._iter_own_markers_closest_first(), overridden by Class to walk MRO in natural closest-first order while preserving decorator stacking order within each class
  • Does not change own_markers order — it stays in base-first (construction) order for backward compatibility
  • Reverses usefixtures marker iteration to maintain farthest-first fixture setup ordering

Key difference from #14332

PR #14332 reverses the MRO traversal in get_unpacked_marks, which changes own_markers order on Class nodes. This approach keeps own_markers unchanged and instead fixes the iteration layer (iter_markers / get_closest_marker), which is the actual consumer that needs closest-first semantics.

This avoids:

  • Changing own_markers order (observable by plugins)
  • Breaking parametrize naming order (which naive reversal causes)
  • Changing keywords dict values

Test plan

  • New test test_mark_closest_mro verifying correct closest-first MRO marker resolution
  • Existing test_mark_mro / get_unpacked_marks test passes unchanged (own_markers order preserved)
  • Existing parametrize ordering tests pass (decorator stacking order preserved within each class)
  • 647 tests passing in testing/test_mark.py, testing/test_nodes.py, testing/python/
  • Full CI

Made with Cursor

…O marker

Fix get_closest_marker and iter_markers to return markers in correct
closest-first order when class inheritance (MRO) is involved (pytest-dev#14329).

Previously, own_markers on Class nodes stored MRO-inherited markers in
base-first (farthest) order, and iter_markers yielded them in that same
order. This caused get_closest_marker to return a base class marker
instead of the overriding child class marker.

The fix introduces _iter_own_markers_closest_first() on Node, overridden
by Class to walk the MRO in natural closest-first order while preserving
decorator stacking order within each class. This avoids changing
own_markers (keeping its base-first construction order) and avoids
breaking parametrize naming order.

Also reverses usefixtures marker iteration to maintain farthest-first
setup ordering (module -> base class -> child class -> function).

Alternative structural approach to PR pytest-dev#14332 that preserves own_markers
order for backward compatibility.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4.6 <claude@anthropic.com>
Comment thread src/_pytest/python.py
"""The public constructor."""
return super().from_parent(name=name, parent=parent, **kw)

def _iter_own_markers_closest_first(self) -> Iterator[Mark]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def _iter_own_markers_closest_first(self) -> Iterator[Mark]:
@override
def _iter_own_markers_closest_first(self) -> Iterator[Mark]:

Comment thread src/_pytest/python.py
mro_mark_ids: set[int] = set()
for cls in self.obj.__mro__:
cls_marks = cls.__dict__.get("pytestmark", [])
if not isinstance(cls_marks, list):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we require a list, or any sequence would do?

Suggested change
if not isinstance(cls_marks, list):
if not isinstance(cls_marks, Sequence):

Comment thread src/_pytest/python.py
Comment on lines +771 to +774
# Yield any dynamically added markers (via add_marker) not from MRO.
for mark in self.own_markers:
if id(mark) not in mro_mark_ids:
yield mark

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems we should add a test specifically for this behavior

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants