diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 7de8f9f54..0e42cd8de 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -579,14 +579,17 @@ class NodeKeywords(MutableMapping[str, Any]): raise ValueError("cannot delete key in keywords dict") def __iter__(self) -> Iterator[str]: + # Doesn't need to be fast. yield from self._markers if self.parent is not None: - yield from self.parent.keywords + for keyword in self.parent.keywords: + # self._marks and self.parent.keywords can have duplicates. + if keyword not in self._markers: + yield keyword def __len__(self) -> int: - return len(self._markers) + ( - len(self.parent.keywords) if self.parent is not None else 0 - ) + # Doesn't need to be fast. + return sum(1 for keyword in self) def __repr__(self) -> str: return f"" diff --git a/testing/test_collection.py b/testing/test_collection.py index 1ea569f71..6532959cb 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -793,7 +793,7 @@ def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None: res.stdout.fnmatch_lines(["*1 passed*"]) -class TestNodekeywords: +class TestNodeKeywords: def test_no_under(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol( """ @@ -859,6 +859,24 @@ class TestNodekeywords: reprec = pytester.inline_run("-k " + expression) reprec.assertoutcome(passed=num_matching_tests, failed=0) + def test_duplicates_handled_correctly(self, pytester: Pytester) -> None: + item = pytester.getitem( + """ + import pytest + pytestmark = pytest.mark.kw + class TestClass: + pytestmark = pytest.mark.kw + def test_method(self): pass + test_method.kw = 'method' + """, + "test_method", + ) + assert item.parent is not None and item.parent.parent is not None + item.parent.parent.keywords["kw"] = "class" + + assert item.keywords["kw"] == "method" + assert len(item.keywords) == len(set(item.keywords)) + COLLECTION_ERROR_PY_FILES = dict( test_01_failure="""