Skip to content

Commit c4c5fa9

Browse files
RonnyPfannschmidtCursor AIclaude
committed
fix: iter_markers/get_closest_marker now correctly returns closest MRO marker
Fix get_closest_marker and iter_markers to return markers in correct closest-first order when class inheritance (MRO) is involved (#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 #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>
1 parent 67a174f commit c4c5fa9

4 files changed

Lines changed: 77 additions & 2 deletions

File tree

src/_pytest/fixtures.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1841,7 +1841,12 @@ def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
18411841

18421842
def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
18431843
"""Return the names of usefixtures fixtures applicable to node."""
1844-
for marker_node, mark in node.iter_markers_with_node(name="usefixtures"):
1844+
# Reverse order (farthest to closest) is more natural for usefixtures,
1845+
# e.g. want a module-level usefixture to be requested before a class one,
1846+
# a parent class' before a child's, etc.
1847+
for marker_node, mark in reversed(
1848+
list(node.iter_markers_with_node(name="usefixtures"))
1849+
):
18451850
if not mark.args:
18461851
marker_node.warn(
18471852
PytestWarning(

src/_pytest/nodes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None:
330330
def iter_markers(self, name: str | None = None) -> Iterator[Mark]:
331331
"""Iterate over all markers of the node.
332332
333+
The markers are returned from closest to farthest.
334+
333335
:param name: If given, filter the results by the name attribute.
334336
:returns: An iterator of the markers of the node.
335337
"""
@@ -340,14 +342,25 @@ def iter_markers_with_node(
340342
) -> Iterator[tuple[Node, Mark]]:
341343
"""Iterate over all markers of the node.
342344
345+
The markers are returned from closest to farthest.
346+
343347
:param name: If given, filter the results by the name attribute.
344348
:returns: An iterator of (node, mark) tuples.
345349
"""
346350
for node in self.iter_parents():
347-
for mark in node.own_markers:
351+
for mark in node._iter_own_markers_closest_first():
348352
if name is None or getattr(mark, "name", None) == name:
349353
yield node, mark
350354

355+
def _iter_own_markers_closest_first(self) -> Iterable[Mark]:
356+
"""Yield own markers in closest-first order.
357+
358+
For most nodes this is just own_markers in order.
359+
Overridden by nodes whose own_markers contain markers from
360+
multiple levels (e.g. Class nodes with MRO-inherited markers).
361+
"""
362+
return self.own_markers
363+
351364
@overload
352365
def get_closest_marker(self, name: str) -> Mark | None: ...
353366

src/_pytest/python.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,28 @@ def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[o
751751
"""The public constructor."""
752752
return super().from_parent(name=name, parent=parent, **kw)
753753

754+
def _iter_own_markers_closest_first(self) -> Iterator[Mark]:
755+
"""own_markers stores MRO markers in base-first order
756+
(construction order). For closest-first iteration, reverse at the
757+
MRO class-group level while preserving decorator order within
758+
each class."""
759+
from _pytest.mark.structures import normalize_mark_list
760+
761+
# Walk MRO in natural order (closest first: Child, Parent, ...)
762+
# yielding each class's marks in their decorator-stacking order.
763+
mro_mark_ids: set[int] = set()
764+
for cls in self.obj.__mro__:
765+
cls_marks = cls.__dict__.get("pytestmark", [])
766+
if not isinstance(cls_marks, list):
767+
cls_marks = [cls_marks]
768+
for mark in normalize_mark_list(cls_marks):
769+
mro_mark_ids.add(id(mark))
770+
yield mark
771+
# Yield any dynamically added markers (via add_marker) not from MRO.
772+
for mark in self.own_markers:
773+
if id(mark) not in mro_mark_ids:
774+
yield mark
775+
754776
def newinstance(self):
755777
return self.obj()
756778

testing/test_mark.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,40 @@ def test_has_inherited(self):
656656
assert has_inherited_marker.kwargs == {"location": "class"}
657657
assert has_own.get_closest_marker("missing") is None
658658

659+
def test_mark_closest_mro(self, pytester: Pytester) -> None:
660+
"""Marks should be collected from MRO from nearest to furthest (#14329)."""
661+
pytester.makepyfile(
662+
"""
663+
import pytest
664+
665+
666+
@pytest.mark.foo(0)
667+
class TestParent:
668+
def test_only_class(self, request):
669+
assert request.node.get_closest_marker("foo").args[0] == 0
670+
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [0]
671+
672+
@pytest.mark.foo(1)
673+
def test_function_and_class(self, request):
674+
assert request.node.get_closest_marker("foo").args[0] == 1
675+
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [1, 0]
676+
677+
678+
@pytest.mark.foo(2)
679+
class TestChild(TestParent):
680+
def test_only_class(self, request):
681+
assert request.node.get_closest_marker("foo").args[0] == 2
682+
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [2, 0]
683+
684+
@pytest.mark.foo(3)
685+
def test_function_and_class(self, request):
686+
assert request.node.get_closest_marker("foo").args[0] == 3
687+
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [3, 2, 0]
688+
"""
689+
)
690+
result = pytester.runpytest()
691+
result.assert_outcomes(passed=4)
692+
659693
def test_mark_with_wrong_marker(self, pytester: Pytester) -> None:
660694
reprec = pytester.inline_runsource(
661695
"""
@@ -1133,6 +1167,7 @@ class TestBarClass(BaseTests):
11331167
def test_addmarker_order(pytester) -> None:
11341168
session = mock.Mock()
11351169
session.own_markers = []
1170+
session._iter_own_markers_closest_first.return_value = session.own_markers
11361171
session.parent = None
11371172
session.nodeid = ""
11381173
session.path = pytester.path

0 commit comments

Comments
 (0)