summaryrefslogtreecommitdiff
path: root/lib/spack/external/_pytest/mark.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/external/_pytest/mark.py')
-rw-r--r--lib/spack/external/_pytest/mark.py291
1 files changed, 214 insertions, 77 deletions
diff --git a/lib/spack/external/_pytest/mark.py b/lib/spack/external/_pytest/mark.py
index 357a60492e..454722ca2c 100644
--- a/lib/spack/external/_pytest/mark.py
+++ b/lib/spack/external/_pytest/mark.py
@@ -1,5 +1,75 @@
""" generic mechanism for marking and selecting python functions. """
+from __future__ import absolute_import, division, print_function
+
import inspect
+import warnings
+from collections import namedtuple
+from operator import attrgetter
+from .compat import imap
+from .deprecated import MARK_PARAMETERSET_UNPACKING
+
+
+def alias(name, warning=None):
+ getter = attrgetter(name)
+
+ def warned(self):
+ warnings.warn(warning, stacklevel=2)
+ return getter(self)
+
+ return property(getter if warning is None else warned, doc='alias for ' + name)
+
+
+class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
+ @classmethod
+ def param(cls, *values, **kw):
+ marks = kw.pop('marks', ())
+ if isinstance(marks, MarkDecorator):
+ marks = marks,
+ else:
+ assert isinstance(marks, (tuple, list, set))
+
+ def param_extract_id(id=None):
+ return id
+
+ id = param_extract_id(**kw)
+ return cls(values, marks, id)
+
+ @classmethod
+ def extract_from(cls, parameterset, legacy_force_tuple=False):
+ """
+ :param parameterset:
+ a legacy style parameterset that may or may not be a tuple,
+ and may or may not be wrapped into a mess of mark objects
+
+ :param legacy_force_tuple:
+ enforce tuple wrapping so single argument tuple values
+ don't get decomposed and break tests
+
+ """
+
+ if isinstance(parameterset, cls):
+ return parameterset
+ if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple:
+ return cls.param(parameterset)
+
+ newmarks = []
+ argval = parameterset
+ while isinstance(argval, MarkDecorator):
+ newmarks.append(MarkDecorator(Mark(
+ argval.markname, argval.args[:-1], argval.kwargs)))
+ argval = argval.args[-1]
+ assert not isinstance(argval, ParameterSet)
+ if legacy_force_tuple:
+ argval = argval,
+
+ if newmarks:
+ warnings.warn(MARK_PARAMETERSET_UNPACKING)
+
+ return cls(argval, marks=newmarks, id=None)
+
+ @property
+ def deprecated_arg_dict(self):
+ return dict((mark.name, mark) for mark in self.marks)
class MarkerError(Exception):
@@ -7,8 +77,8 @@ class MarkerError(Exception):
"""Error in use of a pytest marker/attribute."""
-def pytest_namespace():
- return {'mark': MarkGenerator()}
+def param(*values, **kw):
+ return ParameterSet.param(*values, **kw)
def pytest_addoption(parser):
@@ -21,7 +91,8 @@ def pytest_addoption(parser):
"where all names are substring-matched against test names "
"and their parent classes. Example: -k 'test_method or test_"
"other' matches all test functions and classes whose name "
- "contains 'test_method' or 'test_other'. "
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
+ "matches those that don't contain 'test_method' in their names. "
"Additionally keywords are matched to classes and functions "
"containing extra names in their 'extra_keyword_matches' set, "
"as well as functions which have names assigned directly to them."
@@ -66,7 +137,7 @@ def pytest_collection_modifyitems(items, config):
return
# pytest used to allow "-" for negating
# but today we just allow "-" at the beginning, use "not" instead
- # we probably remove "-" alltogether soon
+ # we probably remove "-" altogether soon
if keywordexpr.startswith("-"):
keywordexpr = "not " + keywordexpr[1:]
selectuntil = False
@@ -96,6 +167,7 @@ def pytest_collection_modifyitems(items, config):
class MarkMapping:
"""Provides a local mapping for markers where item access
resolves to True if the marker is present. """
+
def __init__(self, keywords):
mymarks = set()
for key, value in keywords.items():
@@ -111,6 +183,7 @@ class KeywordMapping:
"""Provides a local mapping for keywords.
Given a list of names, map any substring of one of these names to True.
"""
+
def __init__(self, names):
self._names = names
@@ -162,9 +235,13 @@ def matchkeyword(colitem, keywordexpr):
def pytest_configure(config):
- import pytest
+ config._old_mark_config = MARK_GEN._config
if config.option.strict:
- pytest.mark._config = config
+ MARK_GEN._config = config
+
+
+def pytest_unconfigure(config):
+ MARK_GEN._config = getattr(config, '_old_mark_config', None)
class MarkGenerator:
@@ -178,13 +255,14 @@ class MarkGenerator:
will set a 'slowtest' :class:`MarkInfo` object
on the ``test_function`` object. """
+ _config = None
def __getattr__(self, name):
if name[0] == "_":
raise AttributeError("Marker name must NOT start with underscore")
- if hasattr(self, '_config'):
+ if self._config is not None:
self._check(name)
- return MarkDecorator(name)
+ return MarkDecorator(Mark(name, (), {}))
def _check(self, name):
try:
@@ -192,18 +270,21 @@ class MarkGenerator:
return
except AttributeError:
pass
- self._markers = l = set()
+ self._markers = values = set()
for line in self._config.getini("markers"):
- beginning = line.split(":", 1)
- x = beginning[0].split("(", 1)[0]
- l.add(x)
+ marker, _ = line.split(":", 1)
+ marker = marker.rstrip()
+ x = marker.split("(", 1)[0]
+ values.add(x)
if name not in self._markers:
raise AttributeError("%r not a registered marker" % (name,))
+
def istestfunc(func):
return hasattr(func, "__call__") and \
getattr(func, "__name__", "<lambda>") != "<lambda>"
+
class MarkDecorator:
""" A decorator for test functions and test classes. When applied
it will create :class:`MarkInfo` objects which may be
@@ -237,19 +318,35 @@ class MarkDecorator:
additional keyword or positional arguments.
"""
- def __init__(self, name, args=None, kwargs=None):
- self.name = name
- self.args = args or ()
- self.kwargs = kwargs or {}
+
+ def __init__(self, mark):
+ assert isinstance(mark, Mark), repr(mark)
+ self.mark = mark
+
+ name = alias('mark.name')
+ args = alias('mark.args')
+ kwargs = alias('mark.kwargs')
@property
def markname(self):
- return self.name # for backward-compat (2.4.1 had this attr)
+ return self.name # for backward-compat (2.4.1 had this attr)
+
+ def __eq__(self, other):
+ return self.mark == other.mark if isinstance(other, MarkDecorator) else False
def __repr__(self):
- d = self.__dict__.copy()
- name = d.pop('name')
- return "<MarkDecorator %r %r>" % (name, d)
+ return "<MarkDecorator %r>" % (self.mark,)
+
+ def with_args(self, *args, **kwargs):
+ """ return a MarkDecorator with extra arguments added
+
+ unlike call this can be used even if the sole argument is a callable/class
+
+ :return: MarkDecorator
+ """
+
+ mark = Mark(self.name, args, kwargs)
+ return self.__class__(self.mark.combined_with(mark))
def __call__(self, *args, **kwargs):
""" if passed a single callable argument: decorate it with mark info.
@@ -259,70 +356,110 @@ class MarkDecorator:
is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class):
if is_class:
- if hasattr(func, 'pytestmark'):
- mark_list = func.pytestmark
- if not isinstance(mark_list, list):
- mark_list = [mark_list]
- # always work on a copy to avoid updating pytestmark
- # from a superclass by accident
- mark_list = mark_list + [self]
- func.pytestmark = mark_list
- else:
- func.pytestmark = [self]
+ store_mark(func, self.mark)
else:
- holder = getattr(func, self.name, None)
- if holder is None:
- holder = MarkInfo(
- self.name, self.args, self.kwargs
- )
- setattr(func, self.name, holder)
- else:
- holder.add(self.args, self.kwargs)
+ store_legacy_markinfo(func, self.mark)
+ store_mark(func, self.mark)
return func
- kw = self.kwargs.copy()
- kw.update(kwargs)
- args = self.args + args
- return self.__class__(self.name, args=args, kwargs=kw)
-
-
-def extract_argvalue(maybe_marked_args):
- # TODO: incorrect mark data, the old code wanst able to collect lists
- # individual parametrized argument sets can be wrapped in a series
- # of markers in which case we unwrap the values and apply the mark
- # at Function init
- newmarks = {}
- argval = maybe_marked_args
- while isinstance(argval, MarkDecorator):
- newmark = MarkDecorator(argval.markname,
- argval.args[:-1], argval.kwargs)
- newmarks[newmark.markname] = newmark
- argval = argval.args[-1]
- return argval, newmarks
-
-
-class MarkInfo:
+ return self.with_args(*args, **kwargs)
+
+
+def get_unpacked_marks(obj):
+ """
+ obtain the unpacked marks that are stored on a object
+ """
+ mark_list = getattr(obj, 'pytestmark', [])
+
+ if not isinstance(mark_list, list):
+ mark_list = [mark_list]
+ return [
+ getattr(mark, 'mark', mark) # unpack MarkDecorator
+ for mark in mark_list
+ ]
+
+
+def store_mark(obj, mark):
+ """store a Mark on a object
+ this is used to implement the Mark declarations/decorators correctly
+ """
+ assert isinstance(mark, Mark), mark
+ # always reassign name to avoid updating pytestmark
+ # in a reference that was only borrowed
+ obj.pytestmark = get_unpacked_marks(obj) + [mark]
+
+
+def store_legacy_markinfo(func, mark):
+ """create the legacy MarkInfo objects and put them onto the function
+ """
+ if not isinstance(mark, 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)
+ setattr(func, mark.name, holder)
+ else:
+ holder.add_mark(mark)
+
+
+class Mark(namedtuple('Mark', 'name, args, kwargs')):
+
+ def combined_with(self, other):
+ assert self.name == other.name
+ return Mark(
+ self.name, self.args + other.args,
+ dict(self.kwargs, **other.kwargs))
+
+
+class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """
- def __init__(self, name, args, kwargs):
- #: name of attribute
- self.name = name
- #: positional argument list, empty if none specified
- self.args = args
- #: keyword argument dictionary, empty if nothing specified
- self.kwargs = kwargs.copy()
- self._arglist = [(args, kwargs.copy())]
+
+ def __init__(self, mark):
+ assert isinstance(mark, Mark), repr(mark)
+ self.combined = mark
+ self._marks = [mark]
+
+ name = alias('combined.name')
+ args = alias('combined.args')
+ kwargs = alias('combined.kwargs')
def __repr__(self):
- return "<MarkInfo %r args=%r kwargs=%r>" % (
- self.name, self.args, self.kwargs
- )
+ return "<MarkInfo {0!r}>".format(self.combined)
- def add(self, args, kwargs):
+ def add_mark(self, mark):
""" add a MarkInfo with the given args and kwargs. """
- self._arglist.append((args, kwargs))
- self.args += args
- self.kwargs.update(kwargs)
+ self._marks.append(mark)
+ self.combined = self.combined.combined_with(mark)
def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """
- for args, kwargs in self._arglist:
- yield MarkInfo(self.name, args, kwargs)
+ return imap(MarkInfo, self._marks)
+
+
+MARK_GEN = MarkGenerator()
+
+
+def _marked(func, mark):
+ """ Returns True if :func: is already marked with :mark:, False otherwise.
+ This can happen if marker is applied to class and the test file is
+ invoked more than once.
+ """
+ try:
+ func_mark = getattr(func, mark.name)
+ except AttributeError:
+ return False
+ return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
+
+
+def transfer_markers(funcobj, cls, mod):
+ """
+ this function transfers class level markers and module level markers
+ into function level markinfo objects
+
+ this is the main reason why marks are so broken
+ the resolution will involve phasing out function level MarkInfo objects
+
+ """
+ for obj in (cls, mod):
+ for mark in get_unpacked_marks(obj):
+ if not _marked(funcobj, mark):
+ store_legacy_markinfo(funcobj, mark)