Skip to content

Commit 85fd356

Browse files
committed
1 parent f45fa76 commit 85fd356

File tree

4 files changed

+30
-42
lines changed

4 files changed

+30
-42
lines changed

src/_pytest/mark/structures.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import collections.abc
22
import inspect
3+
import operator
34
import warnings
45
from typing import Any
56
from typing import Callable
@@ -369,28 +370,30 @@ def get_unpacked_marks(obj: object) -> Iterable[Mark]:
369370
return mark_list
370371

371372

372-
def get_unpacked_marks(obj) -> List[Mark]:
373-
"""Obtain the unpacked marks that are stored on an object."""
374-
return normalize_mark_list(get_unpacked_marks(obj))
373+
def marks_to_dict(marks: Iterable[Mark]) -> Dict[str, Mark]:
374+
return {mark.name if mark.name != "parametrize" else mark.args[0]: mark for mark in marks}
375375

376376

377-
def extract_mro_markers(cls):
378-
if cls is None or getattr(cls, "mro_markers", []) or cls is object:
379-
return
377+
def get_mro_marks(cls: type) -> Dict[str, Mark]:
378+
if cls is object or cls is None:
379+
return {}
380+
if hasattr(cls, "mro_markers"):
381+
return getattr(cls, "mro_markers")
380382

381-
markers = {str(mark): mark for mark in get_unpacked_marks(cls) if mark.name != "parametrize"}
382-
for parent_obj in cls.__mro__[::-1][:-1]:
383-
if parent_obj is object:
384-
continue
385-
if not getattr(parent_obj, "mro_markers", []):
386-
extract_mro_markers(parent_obj)
383+
markers_dict = marks_to_dict(get_unpacked_marks(cls))
384+
for parent_obj in cls.__mro__[1:]:
385+
if parent_obj is not object:
386+
for mark_name, mark in get_mro_marks(parent_obj).items():
387+
if mark_name not in markers_dict:
388+
markers_dict[mark_name] = mark
387389

388-
[
389-
markers.__setitem__(str(mark), mark)
390-
for mark in parent_obj.mro_markers
391-
if mark.name != "parametrize"
392-
]
393-
setattr(cls, "mro_markers", list(markers.values()))
390+
# The method is recursive, and it's called for each class.
391+
# To not extract the marks for each item's classes, I store the variable in "cls" as variable cached.
392+
setattr(cls, "mro_markers", markers_dict)
393+
# marks = dict(markers_dict, **{mark.name: mark for mark in getattr(cls, "pytestmark", [])})
394+
# setattr(cls, "pytestmark", list(marks.values()))
395+
# return marks
396+
return markers_dict
394397

395398

396399
def normalize_mark_list(

src/_pytest/nodes.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,14 +378,10 @@ def iter_markers_with_node(
378378
:param name: If given, filter the results by the name attribute.
379379
:returns: An iterator of (node, mark) tuples.
380380
"""
381-
duplicate_markers = set()
382381
for node in reversed(self.listchain()):
383382
for mark in node.own_markers:
384-
if (name is None or getattr(mark, "name", None) == name) and str(
385-
mark
386-
) not in duplicate_markers:
383+
if name is None or getattr(mark, "name", None) == name:
387384
yield node, mark
388-
duplicate_markers.add(str(mark))
389385

390386
@overload
391387
def get_closest_marker(self, name: str) -> Optional[Mark]:

src/_pytest/python.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@
6363
from _pytest.main import Session
6464
from _pytest.mark import MARK_GEN
6565
from _pytest.mark import ParameterSet
66-
from _pytest.mark.structures import extract_mro_markers
67-
from _pytest.mark.structures import get_marks_as_list
66+
from _pytest.mark.structures import get_mro_marks, marks_to_dict
6867
from _pytest.mark.structures import get_unpacked_marks
6968
from _pytest.mark.structures import Mark
7069
from _pytest.mark.structures import MarkDecorator
@@ -1619,10 +1618,9 @@ def __init__(
16191618
# to a readonly property that returns FunctionDefinition.name.
16201619

16211620
self.keywords.update(self.obj.__dict__)
1622-
self.own_markers.extend(get_marks_as_list(self.obj))
1621+
self.own_markers.extend(get_unpacked_marks(self.obj))
16231622
if self.cls:
1624-
extract_mro_markers(self.cls)
1625-
self.own_markers.extend(getattr(self.cls, "mro_markers"))
1623+
self.own_markers[:] = list(dict(get_mro_marks(self.cls), **marks_to_dict(self.own_markers)).values())
16261624
if callspec:
16271625
self.callspec = callspec
16281626
# this is total hostile and a mess

testing/test_mark.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ def test_func():
600600
)
601601
values = reprec.getfailedcollections()
602602
assert len(values) == 1
603-
assert "TypeError" in str(values[0].longrepr)
603+
assert "AttributeError: type object 'pytestmark' has no attribute 'name'" in str(values[0].longrepr)
604604

605605
def test_mark_dynamically_in_funcarg(self, pytester: Pytester) -> None:
606606
pytester.makeconftest(
@@ -1129,16 +1129,7 @@ def test_foo():
11291129
assert result.ret == ExitCode.USAGE_ERROR
11301130

11311131

1132-
@pytest.mark.parametrize(
1133-
"markers",
1134-
(
1135-
"mark1",
1136-
"mark1 and mark2",
1137-
"mark1 and mark2 and mark3",
1138-
"mark1 and mark2 and mark3 and mark4",
1139-
),
1140-
)
1141-
def test_markers_from_multiple_inheritances(pytester: Pytester, markers) -> None:
1132+
def test_markers_from_multiple_inheritances(pytester: Pytester, request) -> None:
11421133
py_file = pytester.makepyfile(
11431134
"""
11441135
import pytest
@@ -1153,9 +1144,9 @@ class Base2:
11531144
@pytest.mark.mark3
11541145
class TestMultipleInheritances(Base1, Base2):
11551146
@pytest.mark.mark4
1156-
def test_multiple_inheritances(self):
1157-
pass
1147+
def test_multiple_inheritances(self, request):
1148+
assert {mark.name for mark in request.node.iter_markers()} == {"mark1","mark2","mark3","mark4",}
11581149
"""
11591150
)
1160-
result = pytester.inline_run(py_file, "-m", markers)
1151+
result = pytester.inline_run()
11611152
result.assertoutcome(passed=1)

0 commit comments

Comments
 (0)