diff options
Diffstat (limited to 'lib/spack/external/_pytest/mark.py')
-rw-r--r-- | lib/spack/external/_pytest/mark.py | 291 |
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) |