Skip to content

Commit 7153370

Browse files
authored
Merge pull request #3317 from RonnyPfannschmidt/marker-pristine-node-storage
introduce a distinct searchable non-broken storage for markers
2 parents 2962c73 + 4df8f2b commit 7153370

File tree

18 files changed

+261
-111
lines changed

18 files changed

+261
-111
lines changed

_pytest/deprecated.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class RemovedInPytest4Warning(DeprecationWarning):
3232
)
3333

3434
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
35-
"MarkInfo objects are deprecated as they contain the merged marks"
35+
"MarkInfo objects are deprecated as they contain the merged marks.\n"
36+
"Please use node.iter_markers to iterate over markers correctly"
3637
)
3738

3839
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(

_pytest/fixtures.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import warnings
77
from collections import OrderedDict, deque, defaultdict
8+
from more_itertools import flatten
89

910
import attr
1011
import py
@@ -371,10 +372,7 @@ def applymarker(self, marker):
371372
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
372373
created by a call to ``pytest.mark.NAME(...)``.
373374
"""
374-
try:
375-
self.node.keywords[marker.markname] = marker
376-
except AttributeError:
377-
raise ValueError(marker)
375+
self.node.add_marker(marker)
378376

379377
def raiseerror(self, msg):
380378
""" raise a FixtureLookupError with the given message. """
@@ -985,10 +983,9 @@ def getfixtureinfo(self, node, func, cls, funcargs=True):
985983
argnames = getfuncargnames(func, cls=cls)
986984
else:
987985
argnames = ()
988-
usefixtures = getattr(func, "usefixtures", None)
986+
usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.name == "usefixtures")
989987
initialnames = argnames
990-
if usefixtures is not None:
991-
initialnames = usefixtures.args + initialnames
988+
initialnames = tuple(usefixtures) + initialnames
992989
fm = node.session._fixturemanager
993990
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames,
994991
node)
@@ -1070,6 +1067,8 @@ def pytest_generate_tests(self, metafunc):
10701067
fixturedef = faclist[-1]
10711068
if fixturedef.params is not None:
10721069
parametrize_func = getattr(metafunc.function, 'parametrize', None)
1070+
if parametrize_func is not None:
1071+
parametrize_func = parametrize_func.combined
10731072
func_params = getattr(parametrize_func, 'args', [[None]])
10741073
func_kwargs = getattr(parametrize_func, 'kwargs', {})
10751074
# skip directly parametrized arguments

_pytest/mark/evaluate.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import platform
55
import traceback
66

7-
from . import MarkDecorator, MarkInfo
87
from ..outcomes import fail, TEST_OUTCOME
98

109

@@ -28,22 +27,15 @@ def __init__(self, item, name):
2827
self._mark_name = name
2928

3029
def __bool__(self):
31-
self._marks = self._get_marks()
32-
return bool(self._marks)
30+
# dont cache here to prevent staleness
31+
return bool(self._get_marks())
3332
__nonzero__ = __bool__
3433

3534
def wasvalid(self):
3635
return not hasattr(self, 'exc')
3736

3837
def _get_marks(self):
39-
40-
keyword = self.item.keywords.get(self._mark_name)
41-
if isinstance(keyword, MarkDecorator):
42-
return [keyword.mark]
43-
elif isinstance(keyword, MarkInfo):
44-
return [x.combined for x in keyword]
45-
else:
46-
return []
38+
return [x for x in self.item.iter_markers() if x.name == self._mark_name]
4739

4840
def invalidraise(self, exc):
4941
raises = self.get('raises')

_pytest/mark/structures.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import inspect
55

66
import attr
7-
from ..deprecated import MARK_PARAMETERSET_UNPACKING
7+
8+
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
89
from ..compat import NOTSET, getfslineno
9-
from six.moves import map
10+
from six.moves import map, reduce
1011

1112

1213
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
@@ -113,11 +114,21 @@ def _for_parametrize(cls, argnames, argvalues, func, config):
113114

114115
@attr.s(frozen=True)
115116
class Mark(object):
116-
name = attr.ib()
117-
args = attr.ib()
118-
kwargs = attr.ib()
117+
#: name of the mark
118+
name = attr.ib(type=str)
119+
#: positional arguments of the mark decorator
120+
args = attr.ib(type="List[object]")
121+
#: keyword arguments of the mark decorator
122+
kwargs = attr.ib(type="Dict[str, object]")
119123

120124
def combined_with(self, other):
125+
"""
126+
:param other: the mark to combine with
127+
:type other: Mark
128+
:rtype: Mark
129+
130+
combines by appending aargs and merging the mappings
131+
"""
121132
assert self.name == other.name
122133
return Mark(
123134
self.name, self.args + other.args,
@@ -233,7 +244,7 @@ def store_legacy_markinfo(func, mark):
233244
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
234245
holder = getattr(func, mark.name, None)
235246
if holder is None:
236-
holder = MarkInfo(mark)
247+
holder = MarkInfo.for_mark(mark)
237248
setattr(func, mark.name, holder)
238249
else:
239250
holder.add_mark(mark)
@@ -260,23 +271,29 @@ def _marked(func, mark):
260271
invoked more than once.
261272
"""
262273
try:
263-
func_mark = getattr(func, mark.name)
274+
func_mark = getattr(func, getattr(mark, 'combined', mark).name)
264275
except AttributeError:
265276
return False
266-
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
277+
return any(mark == info.combined for info in func_mark)
267278

268279

280+
@attr.s
269281
class MarkInfo(object):
270282
""" Marking object created by :class:`MarkDecorator` instances. """
271283

272-
def __init__(self, mark):
273-
assert isinstance(mark, Mark), repr(mark)
274-
self.combined = mark
275-
self._marks = [mark]
284+
_marks = attr.ib()
285+
combined = attr.ib(
286+
repr=False,
287+
default=attr.Factory(lambda self: reduce(Mark.combined_with, self._marks),
288+
takes_self=True))
276289

277-
name = alias('combined.name')
278-
args = alias('combined.args')
279-
kwargs = alias('combined.kwargs')
290+
name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
291+
args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
292+
kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)
293+
294+
@classmethod
295+
def for_mark(cls, mark):
296+
return cls([mark])
280297

281298
def __repr__(self):
282299
return "<MarkInfo {0!r}>".format(self.combined)
@@ -288,7 +305,7 @@ def add_mark(self, mark):
288305

289306
def __iter__(self):
290307
""" yield MarkInfo objects each relating to a marking-call. """
291-
return map(MarkInfo, self._marks)
308+
return map(MarkInfo.for_mark, self._marks)
292309

293310

294311
class MarkGenerator(object):
@@ -365,3 +382,33 @@ def __len__(self):
365382

366383
def __repr__(self):
367384
return "<NodeKeywords for node %s>" % (self.node, )
385+
386+
387+
@attr.s(cmp=False, hash=False)
388+
class NodeMarkers(object):
389+
"""
390+
internal strucutre for storing marks belongong to a node
391+
392+
..warning::
393+
394+
unstable api
395+
396+
"""
397+
own_markers = attr.ib(default=attr.Factory(list))
398+
399+
def update(self, add_markers):
400+
"""update the own markers
401+
"""
402+
self.own_markers.extend(add_markers)
403+
404+
def find(self, name):
405+
"""
406+
find markers in own nodes or parent nodes
407+
needs a better place
408+
"""
409+
for mark in self.own_markers:
410+
if mark.name == name:
411+
yield mark
412+
413+
def __iter__(self):
414+
return iter(self.own_markers)

_pytest/nodes.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import _pytest
99
import _pytest._code
1010

11-
from _pytest.mark.structures import NodeKeywords
11+
from _pytest.mark.structures import NodeKeywords, MarkInfo
1212

1313
SEP = "/"
1414

@@ -90,6 +90,9 @@ def __init__(self, name, parent=None, config=None, session=None, fspath=None, no
9090
#: keywords/markers collected from all scopes
9191
self.keywords = NodeKeywords(self)
9292

93+
#: the marker objects belonging to this node
94+
self.own_markers = []
95+
9396
#: allow adding of extra keywords to use for matching
9497
self.extra_keyword_matches = set()
9598

@@ -178,15 +181,34 @@ def add_marker(self, marker):
178181
elif not isinstance(marker, MarkDecorator):
179182
raise ValueError("is not a string or pytest.mark.* Marker")
180183
self.keywords[marker.name] = marker
184+
self.own_markers.append(marker)
185+
186+
def iter_markers(self):
187+
"""
188+
iterate over all markers of the node
189+
"""
190+
return (x[1] for x in self.iter_markers_with_node())
191+
192+
def iter_markers_with_node(self):
193+
"""
194+
iterate over all markers of the node
195+
returns sequence of tuples (node, mark)
196+
"""
197+
for node in reversed(self.listchain()):
198+
for mark in node.own_markers:
199+
yield node, mark
181200

182201
def get_marker(self, name):
183202
""" get a marker object from this node or None if
184-
the node doesn't have a marker with that name. """
185-
val = self.keywords.get(name, None)
186-
if val is not None:
187-
from _pytest.mark import MarkInfo, MarkDecorator
188-
if isinstance(val, (MarkDecorator, MarkInfo)):
189-
return val
203+
the node doesn't have a marker with that name.
204+
205+
..warning::
206+
207+
deprecated
208+
"""
209+
markers = [x for x in self.iter_markers() if x.name == name]
210+
if markers:
211+
return MarkInfo(markers)
190212

191213
def listextrakeywords(self):
192214
""" Return a set of all extra keywords in self and any parents."""

0 commit comments

Comments
 (0)