From 4692b4b9f1098cb53f884dfc9da1fe581c58910c Mon Sep 17 00:00:00 2001 From: ace2016 <23010364+ace2016@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:58:14 +0100 Subject: [PATCH] Accept MarkDecorator as get_closest_marker default `Mark` is private and warns when instantiated directly, which left no supported way to build the `default` argument. Accept a `MarkDecorator` such as `pytest.mark.foo(1)` and unwrap it to its `Mark`. Closes #13354 Co-Authored-By: Claude Opus 5 --- AUTHORS | 1 + changelog/13354.improvement.rst | 3 +++ src/_pytest/nodes.py | 13 ++++++++++--- testing/test_mark.py | 12 ++++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 changelog/13354.improvement.rst diff --git a/AUTHORS b/AUTHORS index 2154e3babbb..758a74a9362 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,6 +7,7 @@ Aaron Coleman Abdeali JK Abdelrahman Elbehery Abhijeet Kasurde +ace2016 Adam Johnson Adam Stewart Adam Uhlir diff --git a/changelog/13354.improvement.rst b/changelog/13354.improvement.rst new file mode 100644 index 00000000000..9b90b284cd2 --- /dev/null +++ b/changelog/13354.improvement.rst @@ -0,0 +1,3 @@ +:meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` now also accepts a :class:`~pytest.MarkDecorator` as its ``default``, for example ``item.get_closest_marker("foo", pytest.mark.foo(1))``, and returns the wrapped :class:`~pytest.Mark`. + +Previously the only way to build a ``default`` was to instantiate :class:`~pytest.Mark` directly, which is private and warns. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index f0629c2daf7..c6d245de6c9 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -352,15 +352,22 @@ def iter_markers_with_node( def get_closest_marker(self, name: str) -> Mark | None: ... @overload - def get_closest_marker(self, name: str, default: Mark) -> Mark: ... + def get_closest_marker(self, name: str, default: Mark | MarkDecorator) -> Mark: ... - def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None: + def get_closest_marker( + self, name: str, default: Mark | MarkDecorator | None = None + ) -> Mark | None: """Return the first marker matching the name, from closest (for example function) to farther level (for example module level). - :param default: Fallback return value if no marker was found. + :param default: + Fallback return value if no marker was found. A + :class:`~pytest.MarkDecorator` such as ``pytest.mark.foo(1)`` is + also accepted, in which case its :class:`~pytest.Mark` is returned. :param name: Name to filter by. """ + if isinstance(default, MarkDecorator): + default = default.mark return next(self.iter_markers(name=name), default) def listextrakeywords(self) -> set[str]: diff --git a/testing/test_mark.py b/testing/test_mark.py index c70376e7015..7cc63a240af 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -657,6 +657,18 @@ 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_default_mark_decorator(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + def test_without_mark(): + pass + """ + ) + items, _rec = pytester.inline_genitems(p) + (item,) = items + default = pytest.mark.foo(location="default") + assert item.get_closest_marker("foo", default) is default.mark + def test_mark_with_wrong_marker(self, pytester: Pytester) -> None: reprec = pytester.inline_runsource( """