diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 5dfba97ce..383479e07 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -34,6 +34,7 @@ pytest 6.2.5 (2021-08-29) Bug Fixes --------- +- `#9175 `_: Fix duplicate parametrizes in the same module. - `#7792 `_: Fix missing marks when inheritance from multiple classes. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 053297fa2..1abda3596 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -24,6 +24,7 @@ import attr from .._code import getfslineno from ..compat import ascii_escaped +from ..compat import cached_property from ..compat import final from ..compat import NOTSET from ..compat import NotSetType @@ -259,6 +260,11 @@ class Mark: _ispytest=True, ) + @cached_property + def unique_name(self): + # For "parametrize" mark, the name value is "parametrize" and not the name that the user was wrote + return str(self.args[0] if self.name == "parametrize" else self.name) + # A generic parameter designating an object to which a Mark may # be applied -- a test function (callable) or class. @@ -370,17 +376,18 @@ def get_unpacked_marks(obj: object) -> Iterable[Mark]: def get_mro_marks(cls: type): if cls is object or cls is None or hasattr(cls, "mro_markers"): - return getattr(cls, "mro_markers", []) + return getattr(cls, "mro_markers", {}) - # markers = list(mark for mark in get_unpacked_marks(cls) if mark.name != "parametrize") - markers = list(mark for mark in get_unpacked_marks(cls)) + mro_markers = {mark.unique_name: mark for mark in get_unpacked_marks(cls)} for parent_obj in cls.__mro__[1:]: if parent_obj is not object: - markers.extend(get_mro_marks(parent_obj)) + for unique_name, mark in get_mro_marks(parent_obj).items(): + if unique_name not in mro_markers: + mro_markers[unique_name] = mark # To not extract the marks for each item's classes, I store the variable in "cls" as variable cached. - setattr(cls, "mro_markers", list({str(mark): mark for mark in markers}.values())) - return markers + setattr(cls, "mro_markers", mro_markers) + return mro_markers def normalize_mark_list( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6cffff1eb..ac0468ec1 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -382,9 +382,9 @@ class Node(metaclass=NodeMeta): for node in reversed(self.listchain()): for mark in node.own_markers: if name is None or getattr(mark, "name", None) == name: - mark_to_str = str(mark) - if mark_to_str not in duplicate_marks: - duplicate_marks[mark_to_str] = mark + mark_to_name = str(mark) + if mark_to_name not in duplicate_marks: + duplicate_marks[mark_to_name] = mark yield node, mark @overload diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 516bcf4ed..7491df35a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1620,7 +1620,12 @@ class Function(PyobjMixin, nodes.Item): self.keywords.update(self.obj.__dict__) self.own_markers.extend(get_unpacked_marks(self.obj)) if self.cls: - self.own_markers.extend(get_mro_marks(self.cls)) + self.own_markers = list( + dict( + get_mro_marks(self.cls), + **{mark.unique_name: mark for mark in self.own_markers}, + ).values() + ) if callspec: self.callspec = callspec # this is total hostile and a mess diff --git a/testing/test_mark.py b/testing/test_mark.py index 9eecdb59c..9aab5596a 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1130,6 +1130,7 @@ def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: def test_markers_from_multiple_inheritances(pytester: Pytester) -> None: + # https://github.com/pytest-dev/pytest/issues/7792 pytester.makepyfile( """ import pytest @@ -1150,3 +1151,34 @@ def test_markers_from_multiple_inheritances(pytester: Pytester) -> None: ) result = pytester.inline_run() result.assertoutcome(passed=1) + + +def test_duplicate_fixtures_in_same_class(pytester: Pytester) -> None: + # https://github.com/pytest-dev/pytest/issues/9175 + pytester.makepyfile( + """ + from typing import Any + + import pytest + + + @pytest.fixture + def some_fixture(request) -> Any: + return request.param + + + # I apply to all test methods + @pytest.mark.parametrize("some_fixture", [{"b": "b"}], indirect=True) + class TestMultiParameterization: + # I apply to just this one test method + @pytest.mark.parametrize("some_fixture", [{"a": "a"}], indirect=True) + def test_local_some_fixture(self, some_fixture: Any) -> None: + assert "a" in some_fixture + + def test_global_some_fixture(self, some_fixture: Any) -> None: + assert "b" in some_fixture + + """ + ) + result = pytester.inline_run() + result.assertoutcome(passed=2)