Skip to content

Commit 31df38f

Browse files
Merge pull request #10356 from RonnyPfannschmidt/fix-7792-marks-walk-mro
fix #7792: consider marks from the mro
2 parents 15ac034 + c543e0c commit 31df38f

File tree

3 files changed

+59
-7
lines changed

3 files changed

+59
-7
lines changed

changelog/7792.bugfix.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Marks are now inherited according to the full MRO in test classes. Previously, if a test class inherited from two or more classes, only marks from the first super-class would apply.
2+
3+
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
4+
5+
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.

src/_pytest/mark/structures.py

+30-7
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,35 @@ def __call__(self, *args: object, **kwargs: object):
355355
return self.with_args(*args, **kwargs)
356356

357357

358-
def get_unpacked_marks(obj: object) -> Iterable[Mark]:
359-
"""Obtain the unpacked marks that are stored on an object."""
360-
mark_list = getattr(obj, "pytestmark", [])
361-
if not isinstance(mark_list, list):
362-
mark_list = [mark_list]
363-
return normalize_mark_list(mark_list)
358+
def get_unpacked_marks(
359+
obj: Union[object, type],
360+
*,
361+
consider_mro: bool = True,
362+
) -> List[Mark]:
363+
"""Obtain the unpacked marks that are stored on an object.
364+
365+
If obj is a class and consider_mro is true, return marks applied to
366+
this class and all of its super-classes in MRO order. If consider_mro
367+
is false, only return marks applied directly to this class.
368+
"""
369+
if isinstance(obj, type):
370+
if not consider_mro:
371+
mark_lists = [obj.__dict__.get("pytestmark", [])]
372+
else:
373+
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
374+
mark_list = []
375+
for item in mark_lists:
376+
if isinstance(item, list):
377+
mark_list.extend(item)
378+
else:
379+
mark_list.append(item)
380+
else:
381+
mark_attribute = getattr(obj, "pytestmark", [])
382+
if isinstance(mark_attribute, list):
383+
mark_list = mark_attribute
384+
else:
385+
mark_list = [mark_attribute]
386+
return list(normalize_mark_list(mark_list))
364387

365388

366389
def normalize_mark_list(
@@ -388,7 +411,7 @@ def store_mark(obj, mark: Mark) -> None:
388411
assert isinstance(mark, Mark), mark
389412
# Always reassign name to avoid updating pytestmark in a reference that
390413
# was only borrowed.
391-
obj.pytestmark = [*get_unpacked_marks(obj), mark]
414+
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
392415

393416

394417
# Typing for builtin pytest marks. This is cheating; it gives builtin marks

testing/test_mark.py

+24
Original file line numberDiff line numberDiff line change
@@ -1109,3 +1109,27 @@ def test_foo():
11091109
result = pytester.runpytest(foo, "-m", expr)
11101110
result.stderr.fnmatch_lines([expected])
11111111
assert result.ret == ExitCode.USAGE_ERROR
1112+
1113+
1114+
def test_mark_mro() -> None:
1115+
xfail = pytest.mark.xfail
1116+
1117+
@xfail("a")
1118+
class A:
1119+
pass
1120+
1121+
@xfail("b")
1122+
class B:
1123+
pass
1124+
1125+
@xfail("c")
1126+
class C(A, B):
1127+
pass
1128+
1129+
from _pytest.mark.structures import get_unpacked_marks
1130+
1131+
all_marks = get_unpacked_marks(C)
1132+
1133+
assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark]
1134+
1135+
assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]

0 commit comments

Comments
 (0)