From 450c9ed559b8c30a513bbe8b69b0e1d2f984bf13 Mon Sep 17 00:00:00 2001 From: Wintreist <> Date: Sat, 28 Mar 2026 14:41:33 +0300 Subject: [PATCH 1/2] BugFix 14329 --- changelog/14329.bugfix.rst | 1 + src/_pytest/fixtures.py | 4 +++- src/_pytest/mark/structures.py | 4 +--- testing/test_mark.py | 39 +++++++++++++++++++++++++++++++++- 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 changelog/14329.bugfix.rst diff --git a/changelog/14329.bugfix.rst b/changelog/14329.bugfix.rst new file mode 100644 index 00000000000..59fab68bbb3 --- /dev/null +++ b/changelog/14329.bugfix.rst @@ -0,0 +1 @@ +Now the markers are considered in the order from near to far, through the mro. `get_closes_marker("usefixtures")` will also return the nearest mark, but the processing of the `usefixtures` goes from the far mark to the near one. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f667c40ea78..3798eeb05bf 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1682,7 +1682,9 @@ 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"): + 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/testing/test_mark.py b/testing/test_mark.py index 67219313183..c6254bf2935 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -651,6 +651,43 @@ 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_with_parent(self, pytester: Pytester): + test_path = pytester.makepyfile( + """ + import pytest + + @pytest.mark.data(0) + class TestParent: + def get_data_mark(self, request: pytest.FixtureRequest): + getted_mark = request.node.get_closest_marker("data") + assert getted_mark + return getted_mark + + def test_case(self, request: pytest.FixtureRequest): + data_mark = self.get_data_mark(request) + assert data_mark.args[0] == 0 + + @pytest.mark.data(1) + def test_case_with_own_mark(self, request: pytest.FixtureRequest): + data_mark = self.get_data_mark(request) + assert data_mark.args[0] == 1 + + + @pytest.mark.data(2) + class TestChild(TestParent): + def test_case(self, request: pytest.FixtureRequest): + data_mark = self.get_data_mark(request) + assert data_mark.args[0] == 2 + + @pytest.mark.data(3) + def test_case_with_own_mark(self, request: pytest.FixtureRequest): + data_mark = self.get_data_mark(request) + assert data_mark.args[0] == 3 + """ + ) + result = pytester.runpytest(test_path) + result.assert_outcomes(passed=4) + def test_mark_with_wrong_marker(self, pytester: Pytester) -> None: reprec = pytester.inline_runsource( """ @@ -1229,7 +1266,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] From 5d467d35e0dc473577afc4168e76a5ea333f0c8b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 7 Apr 2026 18:14:50 +0300 Subject: [PATCH 2/2] Tweaks --- changelog/14329.bugfix.rst | 3 ++- src/_pytest/fixtures.py | 3 +++ src/_pytest/nodes.py | 4 ++++ testing/test_mark.py | 47 ++++++++++++++++++-------------------- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/changelog/14329.bugfix.rst b/changelog/14329.bugfix.rst index 59fab68bbb3..35748b19ecd 100644 --- a/changelog/14329.bugfix.rst +++ b/changelog/14329.bugfix.rst @@ -1 +1,2 @@ -Now the markers are considered in the order from near to far, through the mro. `get_closes_marker("usefixtures")` will also return the nearest mark, but the processing of the `usefixtures` goes from the far mark to the near one. +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 3798eeb05bf..e7431c90dca 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1682,6 +1682,9 @@ 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.""" + # 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")) ): 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 c6254bf2935..41bcb2424c4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -651,41 +651,38 @@ 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_with_parent(self, pytester: Pytester): - test_path = pytester.makepyfile( + 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.data(0) - class TestParent: - def get_data_mark(self, request: pytest.FixtureRequest): - getted_mark = request.node.get_closest_marker("data") - assert getted_mark - return getted_mark - def test_case(self, request: pytest.FixtureRequest): - data_mark = self.get_data_mark(request) - assert data_mark.args[0] == 0 + @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.data(1) - def test_case_with_own_mark(self, request: pytest.FixtureRequest): - data_mark = self.get_data_mark(request) - assert data_mark.args[0] == 1 + @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.data(2) + @pytest.mark.foo(2) class TestChild(TestParent): - def test_case(self, request: pytest.FixtureRequest): - data_mark = self.get_data_mark(request) - assert data_mark.args[0] == 2 - - @pytest.mark.data(3) - def test_case_with_own_mark(self, request: pytest.FixtureRequest): - data_mark = self.get_data_mark(request) - assert data_mark.args[0] == 3 + 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(test_path) + result = pytester.runpytest() result.assert_outcomes(passed=4) def test_mark_with_wrong_marker(self, pytester: Pytester) -> None: