Skip to content

Commit

Permalink
Merge pull request #3317 from RonnyPfannschmidt/marker-pristine-node-…
Browse files Browse the repository at this point in the history
…storage

introduce a distinct searchable non-broken storage for markers
  • Loading branch information
nicoddemus authored Apr 9, 2018
2 parents 2962c73 + 4df8f2b commit 7153370
Show file tree
Hide file tree
Showing 18 changed files with 261 additions and 111 deletions.
3 changes: 2 additions & 1 deletion _pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class RemovedInPytest4Warning(DeprecationWarning):
)

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

MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
Expand Down
13 changes: 6 additions & 7 deletions _pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
import warnings
from collections import OrderedDict, deque, defaultdict
from more_itertools import flatten

import attr
import py
Expand Down Expand Up @@ -371,10 +372,7 @@ def applymarker(self, marker):
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``pytest.mark.NAME(...)``.
"""
try:
self.node.keywords[marker.markname] = marker
except AttributeError:
raise ValueError(marker)
self.node.add_marker(marker)

def raiseerror(self, msg):
""" raise a FixtureLookupError with the given message. """
Expand Down Expand Up @@ -985,10 +983,9 @@ def getfixtureinfo(self, node, func, cls, funcargs=True):
argnames = getfuncargnames(func, cls=cls)
else:
argnames = ()
usefixtures = getattr(func, "usefixtures", None)
usefixtures = flatten(mark.args for mark in node.iter_markers() if mark.name == "usefixtures")
initialnames = argnames
if usefixtures is not None:
initialnames = usefixtures.args + initialnames
initialnames = tuple(usefixtures) + initialnames
fm = node.session._fixturemanager
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames,
node)
Expand Down Expand Up @@ -1070,6 +1067,8 @@ def pytest_generate_tests(self, metafunc):
fixturedef = faclist[-1]
if fixturedef.params is not None:
parametrize_func = getattr(metafunc.function, 'parametrize', None)
if parametrize_func is not None:
parametrize_func = parametrize_func.combined
func_params = getattr(parametrize_func, 'args', [[None]])
func_kwargs = getattr(parametrize_func, 'kwargs', {})
# skip directly parametrized arguments
Expand Down
14 changes: 3 additions & 11 deletions _pytest/mark/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import platform
import traceback

from . import MarkDecorator, MarkInfo
from ..outcomes import fail, TEST_OUTCOME


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

def __bool__(self):
self._marks = self._get_marks()
return bool(self._marks)
# dont cache here to prevent staleness
return bool(self._get_marks())
__nonzero__ = __bool__

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

def _get_marks(self):

keyword = self.item.keywords.get(self._mark_name)
if isinstance(keyword, MarkDecorator):
return [keyword.mark]
elif isinstance(keyword, MarkInfo):
return [x.combined for x in keyword]
else:
return []
return [x for x in self.item.iter_markers() if x.name == self._mark_name]

def invalidraise(self, exc):
raises = self.get('raises')
Expand Down
79 changes: 63 additions & 16 deletions _pytest/mark/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import inspect

import attr
from ..deprecated import MARK_PARAMETERSET_UNPACKING

from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
from ..compat import NOTSET, getfslineno
from six.moves import map
from six.moves import map, reduce


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

@attr.s(frozen=True)
class Mark(object):
name = attr.ib()
args = attr.ib()
kwargs = attr.ib()
#: name of the mark
name = attr.ib(type=str)
#: positional arguments of the mark decorator
args = attr.ib(type="List[object]")
#: keyword arguments of the mark decorator
kwargs = attr.ib(type="Dict[str, object]")

def combined_with(self, other):
"""
:param other: the mark to combine with
:type other: Mark
:rtype: Mark
combines by appending aargs and merging the mappings
"""
assert self.name == other.name
return Mark(
self.name, self.args + other.args,
Expand Down Expand Up @@ -233,7 +244,7 @@ def store_legacy_markinfo(func, mark):
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
holder = getattr(func, mark.name, None)
if holder is None:
holder = MarkInfo(mark)
holder = MarkInfo.for_mark(mark)
setattr(func, mark.name, holder)
else:
holder.add_mark(mark)
Expand All @@ -260,23 +271,29 @@ def _marked(func, mark):
invoked more than once.
"""
try:
func_mark = getattr(func, mark.name)
func_mark = getattr(func, getattr(mark, 'combined', mark).name)
except AttributeError:
return False
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
return any(mark == info.combined for info in func_mark)


@attr.s
class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """

def __init__(self, mark):
assert isinstance(mark, Mark), repr(mark)
self.combined = mark
self._marks = [mark]
_marks = attr.ib()
combined = attr.ib(
repr=False,
default=attr.Factory(lambda self: reduce(Mark.combined_with, self._marks),
takes_self=True))

name = alias('combined.name')
args = alias('combined.args')
kwargs = alias('combined.kwargs')
name = alias('combined.name', warning=MARK_INFO_ATTRIBUTE)
args = alias('combined.args', warning=MARK_INFO_ATTRIBUTE)
kwargs = alias('combined.kwargs', warning=MARK_INFO_ATTRIBUTE)

@classmethod
def for_mark(cls, mark):
return cls([mark])

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

def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """
return map(MarkInfo, self._marks)
return map(MarkInfo.for_mark, self._marks)


class MarkGenerator(object):
Expand Down Expand Up @@ -365,3 +382,33 @@ def __len__(self):

def __repr__(self):
return "<NodeKeywords for node %s>" % (self.node, )


@attr.s(cmp=False, hash=False)
class NodeMarkers(object):
"""
internal strucutre for storing marks belongong to a node
..warning::
unstable api
"""
own_markers = attr.ib(default=attr.Factory(list))

def update(self, add_markers):
"""update the own markers
"""
self.own_markers.extend(add_markers)

def find(self, name):
"""
find markers in own nodes or parent nodes
needs a better place
"""
for mark in self.own_markers:
if mark.name == name:
yield mark

def __iter__(self):
return iter(self.own_markers)
36 changes: 29 additions & 7 deletions _pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import _pytest
import _pytest._code

from _pytest.mark.structures import NodeKeywords
from _pytest.mark.structures import NodeKeywords, MarkInfo

SEP = "/"

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

#: the marker objects belonging to this node
self.own_markers = []

#: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set()

Expand Down Expand Up @@ -178,15 +181,34 @@ def add_marker(self, marker):
elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker
self.own_markers.append(marker)

def iter_markers(self):
"""
iterate over all markers of the node
"""
return (x[1] for x in self.iter_markers_with_node())

def iter_markers_with_node(self):
"""
iterate over all markers of the node
returns sequence of tuples (node, mark)
"""
for node in reversed(self.listchain()):
for mark in node.own_markers:
yield node, mark

def get_marker(self, name):
""" get a marker object from this node or None if
the node doesn't have a marker with that name. """
val = self.keywords.get(name, None)
if val is not None:
from _pytest.mark import MarkInfo, MarkDecorator
if isinstance(val, (MarkDecorator, MarkInfo)):
return val
the node doesn't have a marker with that name.
..warning::
deprecated
"""
markers = [x for x in self.iter_markers() if x.name == name]
if markers:
return MarkInfo(markers)

def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents."""
Expand Down
Loading

0 comments on commit 7153370

Please sign in to comment.