From d8c783268c6876379f5a3a0251b72d0d6d22d0cf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 8 Oct 2022 08:18:26 +0200 Subject: [PATCH 1/6] fix #7792: consider marks from the mro closes #9105 as superseeded --- changelog/7792.bugfix.rst | 1 + src/_pytest/mark/structures.py | 29 +++++++++++++++++++++++------ testing/test_mark.py | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 changelog/7792.bugfix.rst diff --git a/changelog/7792.bugfix.rst b/changelog/7792.bugfix.rst new file mode 100644 index 000000000..00e09ebe8 --- /dev/null +++ b/changelog/7792.bugfix.rst @@ -0,0 +1 @@ +Consider the full mro when getting marks from classes. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 800a25c92..e9e006125 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -355,12 +355,29 @@ class MarkDecorator: return self.with_args(*args, **kwargs) -def get_unpacked_marks(obj: object) -> Iterable[Mark]: +def get_unpacked_marks( + obj: object | type, + consider_mro: bool = True, +) -> List[Mark]: """Obtain the unpacked marks that are stored on an object.""" - mark_list = getattr(obj, "pytestmark", []) - if not isinstance(mark_list, list): - mark_list = [mark_list] - return normalize_mark_list(mark_list) + if isinstance(obj, type): + if not consider_mro: + mark_lists = [obj.__dict__.get("pytestmark", [])] + else: + mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__] + mark_list = [] + for item in mark_lists: + if isinstance(item, list): + mark_list.extend(item) + else: + mark_list.append(item) + else: + mark_attribute = getattr(obj, "pytestmark", []) + if isinstance(mark_attribute, list): + mark_list = mark_attribute + else: + mark_list = [mark_attribute] + return list(normalize_mark_list(mark_list)) def normalize_mark_list( @@ -388,7 +405,7 @@ def store_mark(obj, mark: Mark) -> None: assert isinstance(mark, Mark), mark # Always reassign name to avoid updating pytestmark in a reference that # was only borrowed. - obj.pytestmark = [*get_unpacked_marks(obj), mark] + obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] # Typing for builtin pytest marks. This is cheating; it gives builtin marks diff --git a/testing/test_mark.py b/testing/test_mark.py index 65f2581bd..8c20fe8b1 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1109,3 +1109,26 @@ def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: result = pytester.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR + + +def test_mark_mro(): + @pytest.mark.xfail("a") + class A: + pass + + @pytest.mark.xfail("b") + class B: + pass + + @pytest.mark.xfail("c") + class C(A, B): + pass + + from _pytest.mark.structures import get_unpacked_marks + + all_marks = list(get_unpacked_marks(C)) + + nk = [(x.name, x.args[0]) for x in all_marks] + assert nk == [("xfail", "c"), ("xfail", "a"), ("xfail", "b")] + + assert list(get_unpacked_marks(C, consider_mro=False)) == [] From 4e7486d3fb95f97422283fc7f21b24e8aa6ef2c8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 8 Oct 2022 08:28:03 +0200 Subject: [PATCH 2/6] fixup: annotations --- src/_pytest/mark/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index e9e006125..0b1daefd6 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -356,7 +356,7 @@ class MarkDecorator: def get_unpacked_marks( - obj: object | type, + obj: Union[object, type], consider_mro: bool = True, ) -> List[Mark]: """Obtain the unpacked marks that are stored on an object.""" From 13e594a31474136fb185eff7084760f724897d6b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 8 Oct 2022 08:35:26 +0200 Subject: [PATCH 3/6] fixup: mark mro test reformatt --- testing/test_mark.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 8c20fe8b1..6199dc0b3 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1112,23 +1112,24 @@ def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: def test_mark_mro(): - @pytest.mark.xfail("a") + xfail = pytest.mark.xfail + + @xfail("a") class A: pass - @pytest.mark.xfail("b") + @xfail("b") class B: pass - @pytest.mark.xfail("c") + @xfail("c") class C(A, B): pass from _pytest.mark.structures import get_unpacked_marks - all_marks = list(get_unpacked_marks(C)) + all_marks = get_unpacked_marks(C) - nk = [(x.name, x.args[0]) for x in all_marks] - assert nk == [("xfail", "c"), ("xfail", "a"), ("xfail", "b")] + assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark] - assert list(get_unpacked_marks(C, consider_mro=False)) == [] + assert get_unpacked_marks(C, consider_mro=False) == [pytest.mark.xfail("c").mark] From c42bb36009445e0cf1915dd5d1b762639a710675 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 8 Oct 2022 08:35:53 +0200 Subject: [PATCH 4/6] fixup: mark mro test reformatt --- testing/test_mark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 6199dc0b3..ebb70e247 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1132,4 +1132,4 @@ def test_mark_mro(): assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark] - assert get_unpacked_marks(C, consider_mro=False) == [pytest.mark.xfail("c").mark] + assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark] From f13f4360d3ff379144f32b4bbb4b4fa4a7a8cf23 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 12 Oct 2022 10:20:16 +0200 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Ran Benita --- changelog/7792.bugfix.rst | 6 +++++- src/_pytest/mark/structures.py | 8 +++++++- testing/test_mark.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/changelog/7792.bugfix.rst b/changelog/7792.bugfix.rst index 00e09ebe8..8f6563789 100644 --- a/changelog/7792.bugfix.rst +++ b/changelog/7792.bugfix.rst @@ -1 +1,5 @@ -Consider the full mro when getting marks from classes. +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. + +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. + +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. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0b1daefd6..b93bf6ed9 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -357,9 +357,15 @@ class MarkDecorator: def get_unpacked_marks( obj: Union[object, type], + *, consider_mro: bool = True, ) -> List[Mark]: - """Obtain the unpacked marks that are stored on an object.""" + """Obtain the unpacked marks that are stored on an object. + + If obj is a class and consider_mro is true, return marks applied to + this class and all of its super-classes in MRO order. If consider_mro + is false, only return marks applied directly to this class. + """ if isinstance(obj, type): if not consider_mro: mark_lists = [obj.__dict__.get("pytestmark", [])] diff --git a/testing/test_mark.py b/testing/test_mark.py index ebb70e247..e2d1a40c3 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1111,7 +1111,7 @@ def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: assert result.ret == ExitCode.USAGE_ERROR -def test_mark_mro(): +def test_mark_mro() -> None: xfail = pytest.mark.xfail @xfail("a") From c543e0c4e83f63943efb0cb06b6c94990c4eb0a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 08:21:16 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/mark/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index b93bf6ed9..5186c9ea3 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -361,7 +361,7 @@ def get_unpacked_marks( consider_mro: bool = True, ) -> List[Mark]: """Obtain the unpacked marks that are stored on an object. - + If obj is a class and consider_mro is true, return marks applied to this class and all of its super-classes in MRO order. If consider_mro is false, only return marks applied directly to this class.