547 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			547 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
| """ generic mechanism for marking and selecting python functions. """
 | |
| from __future__ import absolute_import, division, print_function
 | |
| 
 | |
| import inspect
 | |
| import keyword
 | |
| import warnings
 | |
| import attr
 | |
| from collections import namedtuple
 | |
| from operator import attrgetter
 | |
| from six.moves import map
 | |
| 
 | |
| from _pytest.config import UsageError
 | |
| from .deprecated import MARK_PARAMETERSET_UNPACKING
 | |
| from .compat import NOTSET, getfslineno
 | |
| 
 | |
| EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
 | |
| 
 | |
| 
 | |
| 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)
 | |
| 
 | |
|     @classmethod
 | |
|     def _for_parametrize(cls, argnames, argvalues, function, config):
 | |
|         if not isinstance(argnames, (tuple, list)):
 | |
|             argnames = [x.strip() for x in argnames.split(",") if x.strip()]
 | |
|             force_tuple = len(argnames) == 1
 | |
|         else:
 | |
|             force_tuple = False
 | |
|         parameters = [
 | |
|             ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
 | |
|             for x in argvalues]
 | |
|         del argvalues
 | |
| 
 | |
|         if not parameters:
 | |
|             mark = get_empty_parameterset_mark(config, argnames, function)
 | |
|             parameters.append(ParameterSet(
 | |
|                 values=(NOTSET,) * len(argnames),
 | |
|                 marks=[mark],
 | |
|                 id=None,
 | |
|             ))
 | |
|         return argnames, parameters
 | |
| 
 | |
| 
 | |
| def get_empty_parameterset_mark(config, argnames, function):
 | |
|     requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
 | |
|     if requested_mark in ('', None, 'skip'):
 | |
|         mark = MARK_GEN.skip
 | |
|     elif requested_mark == 'xfail':
 | |
|         mark = MARK_GEN.xfail(run=False)
 | |
|     else:
 | |
|         raise LookupError(requested_mark)
 | |
|     fs, lineno = getfslineno(function)
 | |
|     reason = "got empty parameter set %r, function %s at %s:%d" % (
 | |
|         argnames, function.__name__, fs, lineno)
 | |
|     return mark(reason=reason)
 | |
| 
 | |
| 
 | |
| class MarkerError(Exception):
 | |
| 
 | |
|     """Error in use of a pytest marker/attribute."""
 | |
| 
 | |
| 
 | |
| def param(*values, **kw):
 | |
|     """Specify a parameter in a `pytest.mark.parametrize`_ call.
 | |
| 
 | |
|     .. code-block:: python
 | |
| 
 | |
|         @pytest.mark.parametrize("test_input,expected", [
 | |
|             ("3+5", 8),
 | |
|             pytest.param("6*9", 42, marks=pytest.mark.xfail),
 | |
|         ])
 | |
|         def test_eval(test_input, expected):
 | |
|             assert eval(test_input) == expected
 | |
| 
 | |
|     :param values: variable args of the values of the parameter set, in order.
 | |
|     :keyword marks: a single mark or a list of marks to be applied to this parameter set.
 | |
|     :keyword str id: the id to attribute to this parameter set.
 | |
|     """
 | |
|     return ParameterSet.param(*values, **kw)
 | |
| 
 | |
| 
 | |
| def pytest_addoption(parser):
 | |
|     group = parser.getgroup("general")
 | |
|     group._addoption(
 | |
|         '-k',
 | |
|         action="store", dest="keyword", default='', metavar="EXPRESSION",
 | |
|         help="only run tests which match the given substring expression. "
 | |
|              "An expression is a python evaluatable expression "
 | |
|              "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', 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."
 | |
|     )
 | |
| 
 | |
|     group._addoption(
 | |
|         "-m",
 | |
|         action="store", dest="markexpr", default="", metavar="MARKEXPR",
 | |
|         help="only run tests matching given mark expression.  "
 | |
|              "example: -m 'mark1 and not mark2'."
 | |
|     )
 | |
| 
 | |
|     group.addoption(
 | |
|         "--markers", action="store_true",
 | |
|         help="show markers (builtin, plugin and per-project ones)."
 | |
|     )
 | |
| 
 | |
|     parser.addini("markers", "markers for test functions", 'linelist')
 | |
|     parser.addini(
 | |
|         EMPTY_PARAMETERSET_OPTION,
 | |
|         "default marker for empty parametersets")
 | |
| 
 | |
| 
 | |
| def pytest_cmdline_main(config):
 | |
|     import _pytest.config
 | |
|     if config.option.markers:
 | |
|         config._do_configure()
 | |
|         tw = _pytest.config.create_terminal_writer(config)
 | |
|         for line in config.getini("markers"):
 | |
|             parts = line.split(":", 1)
 | |
|             name = parts[0]
 | |
|             rest = parts[1] if len(parts) == 2 else ''
 | |
|             tw.write("@pytest.mark.%s:" % name, bold=True)
 | |
|             tw.line(rest)
 | |
|             tw.line()
 | |
|         config._ensure_unconfigure()
 | |
|         return 0
 | |
| 
 | |
| 
 | |
| pytest_cmdline_main.tryfirst = True
 | |
| 
 | |
| 
 | |
| def pytest_collection_modifyitems(items, config):
 | |
|     keywordexpr = config.option.keyword.lstrip()
 | |
|     matchexpr = config.option.markexpr
 | |
|     if not keywordexpr and not matchexpr:
 | |
|         return
 | |
|     # pytest used to allow "-" for negating
 | |
|     # but today we just allow "-" at the beginning, use "not" instead
 | |
|     # we probably remove "-" altogether soon
 | |
|     if keywordexpr.startswith("-"):
 | |
|         keywordexpr = "not " + keywordexpr[1:]
 | |
|     selectuntil = False
 | |
|     if keywordexpr[-1:] == ":":
 | |
|         selectuntil = True
 | |
|         keywordexpr = keywordexpr[:-1]
 | |
| 
 | |
|     remaining = []
 | |
|     deselected = []
 | |
|     for colitem in items:
 | |
|         if keywordexpr and not matchkeyword(colitem, keywordexpr):
 | |
|             deselected.append(colitem)
 | |
|         else:
 | |
|             if selectuntil:
 | |
|                 keywordexpr = None
 | |
|             if matchexpr:
 | |
|                 if not matchmark(colitem, matchexpr):
 | |
|                     deselected.append(colitem)
 | |
|                     continue
 | |
|             remaining.append(colitem)
 | |
| 
 | |
|     if deselected:
 | |
|         config.hook.pytest_deselected(items=deselected)
 | |
|         items[:] = remaining
 | |
| 
 | |
| 
 | |
| @attr.s
 | |
| class MarkMapping(object):
 | |
|     """Provides a local mapping for markers where item access
 | |
|     resolves to True if the marker is present. """
 | |
| 
 | |
|     own_mark_names = attr.ib()
 | |
| 
 | |
|     @classmethod
 | |
|     def from_keywords(cls, keywords):
 | |
|         mark_names = set()
 | |
|         for key, value in keywords.items():
 | |
|             if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator):
 | |
|                 mark_names.add(key)
 | |
|         return cls(mark_names)
 | |
| 
 | |
|     def __getitem__(self, name):
 | |
|         return name in self.own_mark_names
 | |
| 
 | |
| 
 | |
| class KeywordMapping(object):
 | |
|     """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
 | |
| 
 | |
|     def __getitem__(self, subname):
 | |
|         for name in self._names:
 | |
|             if subname in name:
 | |
|                 return True
 | |
|         return False
 | |
| 
 | |
| 
 | |
| python_keywords_allowed_list = ["or", "and", "not"]
 | |
| 
 | |
| 
 | |
| def matchmark(colitem, markexpr):
 | |
|     """Tries to match on any marker names, attached to the given colitem."""
 | |
|     return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords))
 | |
| 
 | |
| 
 | |
| def matchkeyword(colitem, keywordexpr):
 | |
|     """Tries to match given keyword expression to given collector item.
 | |
| 
 | |
|     Will match on the name of colitem, including the names of its parents.
 | |
|     Only matches names of items which are either a :class:`Class` or a
 | |
|     :class:`Function`.
 | |
|     Additionally, matches on names in the 'extra_keyword_matches' set of
 | |
|     any item, as well as names directly assigned to test functions.
 | |
|     """
 | |
|     mapped_names = set()
 | |
| 
 | |
|     # Add the names of the current item and any parent items
 | |
|     import pytest
 | |
|     for item in colitem.listchain():
 | |
|         if not isinstance(item, pytest.Instance):
 | |
|             mapped_names.add(item.name)
 | |
| 
 | |
|     # Add the names added as extra keywords to current or parent items
 | |
|     for name in colitem.listextrakeywords():
 | |
|         mapped_names.add(name)
 | |
| 
 | |
|     # Add the names attached to the current function through direct assignment
 | |
|     if hasattr(colitem, 'function'):
 | |
|         for name in colitem.function.__dict__:
 | |
|             mapped_names.add(name)
 | |
| 
 | |
|     mapping = KeywordMapping(mapped_names)
 | |
|     if " " not in keywordexpr:
 | |
|         # special case to allow for simple "-k pass" and "-k 1.3"
 | |
|         return mapping[keywordexpr]
 | |
|     elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
 | |
|         return not mapping[keywordexpr[4:]]
 | |
|     for kwd in keywordexpr.split():
 | |
|         if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list:
 | |
|             raise UsageError("Python keyword '{}' not accepted in expressions passed to '-k'".format(kwd))
 | |
|     try:
 | |
|         return eval(keywordexpr, {}, mapping)
 | |
|     except SyntaxError:
 | |
|         raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr))
 | |
| 
 | |
| 
 | |
| def pytest_configure(config):
 | |
|     config._old_mark_config = MARK_GEN._config
 | |
|     if config.option.strict:
 | |
|         MARK_GEN._config = config
 | |
| 
 | |
|     empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
 | |
| 
 | |
|     if empty_parameterset not in ('skip', 'xfail', None, ''):
 | |
|         raise UsageError(
 | |
|             "{!s} must be one of skip and xfail,"
 | |
|             " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset))
 | |
| 
 | |
| 
 | |
| def pytest_unconfigure(config):
 | |
|     MARK_GEN._config = getattr(config, '_old_mark_config', None)
 | |
| 
 | |
| 
 | |
| class MarkGenerator(object):
 | |
|     """ Factory for :class:`MarkDecorator` objects - exposed as
 | |
|     a ``pytest.mark`` singleton instance.  Example::
 | |
| 
 | |
|          import pytest
 | |
|          @pytest.mark.slowtest
 | |
|          def test_function():
 | |
|             pass
 | |
| 
 | |
|     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 self._config is not None:
 | |
|             self._check(name)
 | |
|         return MarkDecorator(Mark(name, (), {}))
 | |
| 
 | |
|     def _check(self, name):
 | |
|         try:
 | |
|             if name in self._markers:
 | |
|                 return
 | |
|         except AttributeError:
 | |
|             pass
 | |
|         self._markers = values = set()
 | |
|         for line in self._config.getini("markers"):
 | |
|             marker = line.split(":", 1)[0]
 | |
|             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>"
 | |
| 
 | |
| 
 | |
| @attr.s(frozen=True)
 | |
| class Mark(object):
 | |
|     name = attr.ib()
 | |
|     args = attr.ib()
 | |
|     kwargs = attr.ib()
 | |
| 
 | |
|     def combined_with(self, other):
 | |
|         assert self.name == other.name
 | |
|         return Mark(
 | |
|             self.name, self.args + other.args,
 | |
|             dict(self.kwargs, **other.kwargs))
 | |
| 
 | |
| 
 | |
| @attr.s
 | |
| class MarkDecorator(object):
 | |
|     """ A decorator for test functions and test classes.  When applied
 | |
|     it will create :class:`MarkInfo` objects which may be
 | |
|     :ref:`retrieved by hooks as item keywords <excontrolskip>`.
 | |
|     MarkDecorator instances are often created like this::
 | |
| 
 | |
|         mark1 = pytest.mark.NAME              # simple MarkDecorator
 | |
|         mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator
 | |
| 
 | |
|     and can then be applied as decorators to test functions::
 | |
| 
 | |
|         @mark2
 | |
|         def test_function():
 | |
|             pass
 | |
| 
 | |
|     When a MarkDecorator instance is called it does the following:
 | |
|       1. If called with a single class as its only positional argument and no
 | |
|          additional keyword arguments, it attaches itself to the class so it
 | |
|          gets applied automatically to all test cases found in that class.
 | |
|       2. If called with a single function as its only positional argument and
 | |
|          no additional keyword arguments, it attaches a MarkInfo object to the
 | |
|          function, containing all the arguments already stored internally in
 | |
|          the MarkDecorator.
 | |
|       3. When called in any other case, it performs a 'fake construction' call,
 | |
|          i.e. it returns a new MarkDecorator instance with the original
 | |
|          MarkDecorator's content updated with the arguments passed to this
 | |
|          call.
 | |
| 
 | |
|     Note: The rules above prevent MarkDecorator objects from storing only a
 | |
|     single function or class reference as their positional argument with no
 | |
|     additional keyword or positional arguments.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     mark = attr.ib(validator=attr.validators.instance_of(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)
 | |
| 
 | |
|     def __eq__(self, other):
 | |
|         return self.mark == other.mark if isinstance(other, MarkDecorator) else False
 | |
| 
 | |
|     def __repr__(self):
 | |
|         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.
 | |
|             otherwise add *args/**kwargs in-place to mark information. """
 | |
|         if args and not kwargs:
 | |
|             func = args[0]
 | |
|             is_class = inspect.isclass(func)
 | |
|             if len(args) == 1 and (istestfunc(func) or is_class):
 | |
|                 if is_class:
 | |
|                     store_mark(func, self.mark)
 | |
|                 else:
 | |
|                     store_legacy_markinfo(func, self.mark)
 | |
|                     store_mark(func, self.mark)
 | |
|                 return func
 | |
|         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 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]
 | |
| 
 | |
|     name = alias('combined.name')
 | |
|     args = alias('combined.args')
 | |
|     kwargs = alias('combined.kwargs')
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "<MarkInfo {0!r}>".format(self.combined)
 | |
| 
 | |
|     def add_mark(self, mark):
 | |
|         """ add a MarkInfo with the given args and kwargs. """
 | |
|         self._marks.append(mark)
 | |
|         self.combined = self.combined.combined_with(mark)
 | |
| 
 | |
|     def __iter__(self):
 | |
|         """ yield MarkInfo objects each relating to a marking-call. """
 | |
|         return map(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)
 |