diff --git a/changelog/14329.bugfix.rst b/changelog/14329.bugfix.rst new file mode 100644 index 00000000000..35748b19ecd --- /dev/null +++ b/changelog/14329.bugfix.rst @@ -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. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f667c40ea78..e7431c90dca 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -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( diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5c9e6601e8a..c5ad8d02e92 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -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): diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index f0629c2daf7..27a2f6076a0 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -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. """ @@ -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. """ diff --git a/testing/test_mark.py b/testing/test_mark.py index 67219313183..41bcb2424c4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -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( """ @@ -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]