diff --git a/AUTHORS b/AUTHORS index d5a10710306..a02d87e4a1d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -104,6 +104,7 @@ Edison Gustavo Muenz Edoardo Batini Edson Tadeu M. Manoel Eduardo Schettino +Efraimov Oren Eli Boyarski Elizaveta Shashkova Éloi Rivard diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index f994c432f87..afad030c485 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,11 @@ with advance notice in the **Deprecations** section of releases. pytest 6.2.5 (2021-08-29) ========================= +Bug Fixes +--------- + +- `#7792 `_: Fixing missing marks when inheritance from multiple classes. + Trivial/Internal Changes ------------------------ diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 76dff5e082c..766103cfa87 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -4,6 +4,7 @@ from typing import Any from typing import Callable from typing import Collection +from typing import Dict from typing import Iterable from typing import Iterator from typing import List @@ -368,6 +369,24 @@ def get_unpacked_marks(obj) -> List[Mark]: return normalize_mark_list(mark_list) +MARKERS_BY_CLASS_NAME: Dict[str, List[Mark]] = {} + + +def get_all_mro_marks(cls, module_name): + all_markers = getattr(cls, "pytestmark", []) + if cls is None: + return all_markers + if not isinstance(all_markers, list): + all_markers = [all_markers] + for parent_obj in cls.__mro__[::-1]: + full_class_name = f"{module_name}_{parent_obj.__name__}" + if not MARKERS_BY_CLASS_NAME.get(full_class_name): + MARKERS_BY_CLASS_NAME[full_class_name] = get_unpacked_marks(parent_obj) + all_markers.extend(MARKERS_BY_CLASS_NAME[full_class_name]) + setattr(cls, "pytestmark", list({str(mark): mark for mark in all_markers}.values())) + return cls.pytestmark + + def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]: """ Normalize an iterable of Mark or MarkDecorator objects into a list of marks diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e44320ddf49..9fbb9dc617f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -60,6 +60,7 @@ from _pytest.main import Session from _pytest.mark import MARK_GEN from _pytest.mark import ParameterSet +from _pytest.mark.structures import get_all_mro_marks from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator @@ -1586,6 +1587,7 @@ def __init__( # to a readonly property that returns FunctionDefinition.name. self.keywords.update(self.obj.__dict__) + self.own_markers.extend(get_all_mro_marks(self.cls, self.module.__name__)) self.own_markers.extend(get_unpacked_marks(self.obj)) if callspec: self.callspec = callspec diff --git a/testing/test_mark.py b/testing/test_mark.py index 77991f9e273..7d23c957c0c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1127,3 +1127,35 @@ def test_foo(): result = pytester.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR + + +@pytest.mark.parametrize( + "markers", + ( + "mark1", + "mark1 and mark2", + "mark1 and mark2 and mark3", + "mark1 and mark2 and mark3 and mark4", + ), +) +def test_markers_from_multiple_inheritances(pytester: Pytester, markers) -> None: + py_file = pytester.makepyfile( + """ + import pytest + @pytest.mark.mark1 + class Base1: + pass + + @pytest.mark.mark2 + class Base2: + pass + + @pytest.mark.mark3 + class TestMultipleInheritances(Base1, Base2): + @pytest.mark.mark4 + def test_multiple_inheritances(self): + pass + """ + ) + result = pytester.inline_run(py_file, "-m", markers) + result.assertoutcome(passed=1)