fix: iter_markers/get_closest_marker returns correct closest MRO marker (#14329)#14630
Draft
RonnyPfannschmidt wants to merge 1 commit into
Draft
Conversation
…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>
nicoddemus
reviewed
Jun 20, 2026
| """The public constructor.""" | ||
| return super().from_parent(name=name, parent=parent, **kw) | ||
|
|
||
| def _iter_own_markers_closest_first(self) -> Iterator[Mark]: |
Member
There was a problem hiding this comment.
Suggested change
| def _iter_own_markers_closest_first(self) -> Iterator[Mark]: | |
| @override | |
| def _iter_own_markers_closest_first(self) -> Iterator[Mark]: |
| mro_mark_ids: set[int] = set() | ||
| for cls in self.obj.__mro__: | ||
| cls_marks = cls.__dict__.get("pytestmark", []) | ||
| if not isinstance(cls_marks, list): |
Member
There was a problem hiding this comment.
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 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 |
Member
There was a problem hiding this comment.
Seems we should add a test specifically for this behavior
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Alternative approach to #14332 for fixing #14329 (
get_closest_markerreturning the wrong marker with class inheritance).Node._iter_own_markers_closest_first(), overridden byClassto walk MRO in natural closest-first order while preserving decorator stacking order within each classown_markersorder — it stays in base-first (construction) order for backward compatibilityusefixturesmarker iteration to maintain farthest-first fixture setup orderingKey difference from #14332
PR #14332 reverses the MRO traversal in
get_unpacked_marks, which changesown_markersorder on Class nodes. This approach keepsown_markersunchanged and instead fixes the iteration layer (iter_markers/get_closest_marker), which is the actual consumer that needs closest-first semantics.This avoids:
own_markersorder (observable by plugins)keywordsdict valuesTest plan
test_mark_closest_mroverifying correct closest-first MRO marker resolutiontest_mark_mro/get_unpacked_markstest passes unchanged (own_markers order preserved)testing/test_mark.py,testing/test_nodes.py,testing/python/Made with Cursor