Skip to content
Open
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
2 changes: 2 additions & 0 deletions changelog/14329.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed :func:`request.node.get_closest_marker() <_pytest.nodes.Node.get_closest_marker>` (and :func:`iter_markers() <_pytest.nodes.Node.iter_markers>`) traversing MRO in the wrong order (farthest to closest).
This could also affect :func:`pytest.mark.usefixtures` usage.
7 changes: 6 additions & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1682,7 +1682,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 (fartest 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
4 changes: 1 addition & 3 deletions src/_pytest/mark/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,7 @@ def get_unpacked_marks(
if not consider_mro:
mark_lists = [obj.__dict__.get("pytestmark", [])]
else:
mark_lists = [
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
]
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
mark_list = []
for item in mark_lists:
if isinstance(item, list):
Expand Down
4 changes: 4 additions & 0 deletions 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,6 +342,8 @@ 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.
"""
Expand Down
36 changes: 35 additions & 1 deletion testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,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 @@ -1229,7 +1263,7 @@ class C(A, B):

all_marks = get_unpacked_marks(C)

assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark]
assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark]

assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]

Expand Down