Skip to content
Closed
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
3 changes: 3 additions & 0 deletions changelog/10406.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Parameter-set marks now take precedence over marks on the test function,
class, or module. This also fixes ``filterwarnings`` precedence across those
levels while preserving the documented ordering of filters defined together.
6 changes: 5 additions & 1 deletion src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,14 @@ def iter_markers_with_node(
: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]:
"""Iterate over this node's markers from closest to farthest."""
return self.own_markers

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

Expand Down
25 changes: 24 additions & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -1634,7 +1634,10 @@ def __init__(
# Note: when FunctionDefinition is introduced, we should change ``originalname``
# to a readonly property that returns FunctionDefinition.name.

self.own_markers.extend(get_unpacked_marks(self.obj))
function_markers = get_unpacked_marks(self.obj)
self.own_markers.extend(function_markers)
self._function_markers_count = len(function_markers)
self._prepended_markers_count = 0
if callspec:
self.callspec = callspec
self.own_markers.extend(callspec.marks)
Expand All @@ -1656,6 +1659,26 @@ def __init__(
self.fixturenames = fixtureinfo.names_closure
self._initrequest()

def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None:
super().add_marker(marker, append=append)
if not append:
self._prepended_markers_count += 1

def _iter_own_markers_closest_first(self) -> Iterator[Mark]:
"""Yield parameter-set marks before function-level marks."""
if not hasattr(self, "callspec"):
yield from self.own_markers
return

function_start = self._prepended_markers_count
function_end = function_start + self._function_markers_count
callspec_end = function_end + len(self.callspec.marks)

yield from self.own_markers[:function_start]
yield from self.own_markers[function_end:callspec_end]
yield from self.own_markers[function_start:function_end]
yield from self.own_markers[callspec_end:]

# todo: determine sound type limitations
@classmethod
def from_parent(cls, parent, **kw) -> Self:
Expand Down
10 changes: 7 additions & 3 deletions src/_pytest/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ def catch_warnings_for_item(
# apply filters from "filterwarnings" marks
nodeid = "" if item is None else item.nodeid
if item is not None:
for mark in item.iter_markers(name="filterwarnings"):
for arg in mark.args:
warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
for node in reversed(list(item.iter_parents())):
for mark in node.own_markers:
if mark.name == "filterwarnings":
for arg in mark.args:
warnings.filterwarnings(
*parse_warning_filter(arg, escape=False)
)

try:
yield
Expand Down
34 changes: 34 additions & 0 deletions testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,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 Expand Up @@ -1182,6 +1183,39 @@ def test_custom_mark_parametrized(obj_type):
result.assert_outcomes(passed=4)


def test_parametrize_mark_is_closest(pytester: Pytester) -> None:
"""Marks on a parameter set take precedence over function marks (#10406)."""
pytester.makepyfile(
"""
import pytest

pytestmark = pytest.mark.priority("module")

@pytest.mark.priority("class")
class TestMarkers:
@pytest.mark.priority("function")
@pytest.mark.parametrize(
"value",
[pytest.param(None, marks=pytest.mark.priority("parametrize"))],
)
def test_mark_order(self, value, request):
priorities = [
mark.args[0]
for mark in request.node.iter_markers("priority")
]
assert priorities == [
"parametrize",
"function",
"class",
"module",
]
"""
)

result = pytester.runpytest()
result.assert_outcomes(passed=1)


def test_pytest_param_id_requires_string() -> None:
with pytest.raises(TypeError) as excinfo:
pytest.param(id=True) # type: ignore[arg-type]
Expand Down
48 changes: 48 additions & 0 deletions testing/test_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,54 @@ def test():
result.stdout.fnmatch_lines(["* 1 failed in*"])


def test_filterwarnings_mark_hierarchy_precedence(pytester: Pytester) -> None:
"""The closest filterwarnings mark should take precedence (#10406)."""
pytester.makepyfile(
"""
import warnings
import pytest
@pytest.mark.filterwarnings("error")
class TestWarnings:
@pytest.mark.parametrize(
"value",
[
pytest.param(
None,
marks=pytest.mark.filterwarnings("ignore"),
)
],
)
def test_parametrize_over_class(self, value):
warnings.warn("parametrize over class")
@pytest.mark.filterwarnings("error")
@pytest.mark.parametrize(
"value",
[
pytest.param(
None,
marks=pytest.mark.filterwarnings("ignore"),
)
],
)
def test_parametrize_over_function(self, value):
warnings.warn("parametrize over function")
@pytest.mark.filterwarnings("ignore:specific")
@pytest.mark.filterwarnings("error")
def test_same_level_decorator_order():
warnings.warn("specific")
"""
)

result = pytester.runpytest()
result.assert_outcomes(passed=3)


def test_accept_unknown_category(pytester: Pytester) -> None:
"""Category types that can't be imported don't cause failure (#13732)."""
pytester.makeini(
Expand Down