Skip to content
Draft
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
7 changes: 6 additions & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1841,7 +1841,12 @@ def _getautousenames(self, node: nodes.Node) -> Iterator[str]:

def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
"""Return the names of usefixtures fixtures applicable to node."""
for marker_node, mark in node.iter_markers_with_node(name="usefixtures"):
# Reverse order (farthest to closest) is more natural for usefixtures,
# e.g. want a module-level usefixture to be requested before a class one,
# a parent class' before a child's, etc.
for marker_node, mark in reversed(
list(node.iter_markers_with_node(name="usefixtures"))
):
if not mark.args:
marker_node.warn(
PytestWarning(
Expand Down
15 changes: 14 additions & 1 deletion src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None:
def iter_markers(self, name: str | None = None) -> Iterator[Mark]:
"""Iterate over all markers of the node.

The markers are returned from closest to farthest.

:param name: If given, filter the results by the name attribute.
:returns: An iterator of the markers of the node.
"""
Expand All @@ -340,14 +342,25 @@ def iter_markers_with_node(
) -> Iterator[tuple[Node, Mark]]:
"""Iterate over all markers of the node.

The markers are returned from closest to farthest.

:param name: If given, filter the results by the name attribute.
:returns: An iterator of (node, mark) tuples.
"""
for node in self.iter_parents():
for mark in node.own_markers:
for mark in node._iter_own_markers_closest_first():
if name is None or getattr(mark, "name", None) == name:
yield node, mark

def _iter_own_markers_closest_first(self) -> Iterable[Mark]:
"""Yield own markers in closest-first order.

For most nodes this is just own_markers in order.
Overridden by nodes whose own_markers contain markers from
multiple levels (e.g. Class nodes with MRO-inherited markers).
"""
return self.own_markers

@overload
def get_closest_marker(self, name: str) -> Mark | None: ...

Expand Down
22 changes: 22 additions & 0 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,28 @@ def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[o
"""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]:

"""own_markers stores MRO markers in base-first order
(construction order). For closest-first iteration, reverse at the
MRO class-group level while preserving decorator order within
each class."""
from _pytest.mark.structures import normalize_mark_list

# Walk MRO in natural order (closest first: Child, Parent, ...)
# yielding each class's marks in their decorator-stacking order.
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):

cls_marks = [cls_marks]
for mark in normalize_mark_list(cls_marks):
mro_mark_ids.add(id(mark))
yield mark
# 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
Comment on lines +771 to +774

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


def newinstance(self):
return self.obj()

Expand Down
35 changes: 35 additions & 0 deletions testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,40 @@ def test_has_inherited(self):
assert has_inherited_marker.kwargs == {"location": "class"}
assert has_own.get_closest_marker("missing") is None

def test_mark_closest_mro(self, pytester: Pytester) -> None:
"""Marks should be collected from MRO from nearest to furthest (#14329)."""
pytester.makepyfile(
"""
import pytest


@pytest.mark.foo(0)
class TestParent:
def test_only_class(self, request):
assert request.node.get_closest_marker("foo").args[0] == 0
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [0]

@pytest.mark.foo(1)
def test_function_and_class(self, request):
assert request.node.get_closest_marker("foo").args[0] == 1
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [1, 0]


@pytest.mark.foo(2)
class TestChild(TestParent):
def test_only_class(self, request):
assert request.node.get_closest_marker("foo").args[0] == 2
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [2, 0]

@pytest.mark.foo(3)
def test_function_and_class(self, request):
assert request.node.get_closest_marker("foo").args[0] == 3
assert [mark.args[0] for mark in request.node.iter_markers("foo")] == [3, 2, 0]
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=4)

def test_mark_with_wrong_marker(self, pytester: Pytester) -> None:
reprec = pytester.inline_runsource(
"""
Expand Down Expand Up @@ -1133,6 +1167,7 @@ class TestBarClass(BaseTests):
def test_addmarker_order(pytester) -> None:
session = mock.Mock()
session.own_markers = []
session._iter_own_markers_closest_first.return_value = session.own_markers
session.parent = None
session.nodeid = ""
session.path = pytester.path
Expand Down
Loading