Merge pull request #3250 from RonnyPfannschmidt/mark-package
Mark package
This commit is contained in:
commit
da3f4045e7
|
@ -0,0 +1,141 @@
|
||||||
|
""" generic mechanism for marking and selecting python functions. """
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
from _pytest.config import UsageError
|
||||||
|
from .structures import (
|
||||||
|
ParameterSet, EMPTY_PARAMETERSET_OPTION, MARK_GEN,
|
||||||
|
Mark, MarkInfo, MarkDecorator, MarkGenerator,
|
||||||
|
transfer_markers, get_empty_parameterset_mark
|
||||||
|
)
|
||||||
|
from .legacy import matchkeyword, matchmark
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Mark', 'MarkInfo', 'MarkDecorator', 'MarkGenerator',
|
||||||
|
'transfer_markers', 'get_empty_parameterset_mark'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MarkerError(Exception):
|
||||||
|
|
||||||
|
"""Error in use of a pytest marker/attribute."""
|
||||||
|
|
||||||
|
|
||||||
|
def param(*values, **kw):
|
||||||
|
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 deselect_by_keyword(items, config):
|
||||||
|
keywordexpr = config.option.keyword.lstrip()
|
||||||
|
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
|
||||||
|
remaining.append(colitem)
|
||||||
|
|
||||||
|
if deselected:
|
||||||
|
config.hook.pytest_deselected(items=deselected)
|
||||||
|
items[:] = remaining
|
||||||
|
|
||||||
|
|
||||||
|
def deselect_by_mark(items, config):
|
||||||
|
matchexpr = config.option.markexpr
|
||||||
|
if not matchexpr:
|
||||||
|
return
|
||||||
|
|
||||||
|
remaining = []
|
||||||
|
deselected = []
|
||||||
|
for item in items:
|
||||||
|
if matchmark(item, matchexpr):
|
||||||
|
remaining.append(item)
|
||||||
|
else:
|
||||||
|
deselected.append(item)
|
||||||
|
|
||||||
|
if deselected:
|
||||||
|
config.hook.pytest_deselected(items=deselected)
|
||||||
|
items[:] = remaining
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(items, config):
|
||||||
|
deselect_by_keyword(items, config)
|
||||||
|
deselect_by_mark(items, config)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,125 @@
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from . import MarkDecorator, MarkInfo
|
||||||
|
from ..outcomes import fail, TEST_OUTCOME
|
||||||
|
|
||||||
|
|
||||||
|
def cached_eval(config, expr, d):
|
||||||
|
if not hasattr(config, '_evalcache'):
|
||||||
|
config._evalcache = {}
|
||||||
|
try:
|
||||||
|
return config._evalcache[expr]
|
||||||
|
except KeyError:
|
||||||
|
import _pytest._code
|
||||||
|
exprcode = _pytest._code.compile(expr, mode="eval")
|
||||||
|
config._evalcache[expr] = x = eval(exprcode, d)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
class MarkEvaluator(object):
|
||||||
|
def __init__(self, item, name):
|
||||||
|
self.item = item
|
||||||
|
self._marks = None
|
||||||
|
self._mark = None
|
||||||
|
self._mark_name = name
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
self._marks = self._get_marks()
|
||||||
|
return bool(self._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 []
|
||||||
|
|
||||||
|
def invalidraise(self, exc):
|
||||||
|
raises = self.get('raises')
|
||||||
|
if not raises:
|
||||||
|
return
|
||||||
|
return not isinstance(exc, raises)
|
||||||
|
|
||||||
|
def istrue(self):
|
||||||
|
try:
|
||||||
|
return self._istrue()
|
||||||
|
except TEST_OUTCOME:
|
||||||
|
self.exc = sys.exc_info()
|
||||||
|
if isinstance(self.exc[1], SyntaxError):
|
||||||
|
msg = [" " * (self.exc[1].offset + 4) + "^", ]
|
||||||
|
msg.append("SyntaxError: invalid syntax")
|
||||||
|
else:
|
||||||
|
msg = traceback.format_exception_only(*self.exc[:2])
|
||||||
|
fail("Error evaluating %r expression\n"
|
||||||
|
" %s\n"
|
||||||
|
"%s"
|
||||||
|
% (self._mark_name, self.expr, "\n".join(msg)),
|
||||||
|
pytrace=False)
|
||||||
|
|
||||||
|
def _getglobals(self):
|
||||||
|
d = {'os': os, 'sys': sys, 'config': self.item.config}
|
||||||
|
if hasattr(self.item, 'obj'):
|
||||||
|
d.update(self.item.obj.__globals__)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _istrue(self):
|
||||||
|
if hasattr(self, 'result'):
|
||||||
|
return self.result
|
||||||
|
self._marks = self._get_marks()
|
||||||
|
|
||||||
|
if self._marks:
|
||||||
|
self.result = False
|
||||||
|
for mark in self._marks:
|
||||||
|
self._mark = mark
|
||||||
|
if 'condition' in mark.kwargs:
|
||||||
|
args = (mark.kwargs['condition'],)
|
||||||
|
else:
|
||||||
|
args = mark.args
|
||||||
|
|
||||||
|
for expr in args:
|
||||||
|
self.expr = expr
|
||||||
|
if isinstance(expr, six.string_types):
|
||||||
|
d = self._getglobals()
|
||||||
|
result = cached_eval(self.item.config, expr, d)
|
||||||
|
else:
|
||||||
|
if "reason" not in mark.kwargs:
|
||||||
|
# XXX better be checked at collection time
|
||||||
|
msg = "you need to specify reason=STRING " \
|
||||||
|
"when using booleans as conditions."
|
||||||
|
fail(msg)
|
||||||
|
result = bool(expr)
|
||||||
|
if result:
|
||||||
|
self.result = True
|
||||||
|
self.reason = mark.kwargs.get('reason', None)
|
||||||
|
self.expr = expr
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
self.result = True
|
||||||
|
self.reason = mark.kwargs.get('reason', None)
|
||||||
|
return self.result
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, attr, default=None):
|
||||||
|
if self._mark is None:
|
||||||
|
return default
|
||||||
|
return self._mark.kwargs.get(attr, default)
|
||||||
|
|
||||||
|
def getexplanation(self):
|
||||||
|
expl = getattr(self, 'reason', None) or self.get('reason', None)
|
||||||
|
if not expl:
|
||||||
|
if not hasattr(self, 'expr'):
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return "condition: " + str(self.expr)
|
||||||
|
return expl
|
|
@ -0,0 +1,97 @@
|
||||||
|
"""
|
||||||
|
this is a place where we put datastructures used by legacy apis
|
||||||
|
we hope ot remove
|
||||||
|
"""
|
||||||
|
import attr
|
||||||
|
import keyword
|
||||||
|
|
||||||
|
from . import MarkInfo, MarkDecorator
|
||||||
|
|
||||||
|
from _pytest.config import UsageError
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_item(cls, item):
|
||||||
|
mapped_names = set()
|
||||||
|
|
||||||
|
# Add the names of the current item and any parent items
|
||||||
|
import pytest
|
||||||
|
for item in item.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 item.listextrakeywords():
|
||||||
|
mapped_names.add(name)
|
||||||
|
|
||||||
|
# Add the names attached to the current function through direct assignment
|
||||||
|
if hasattr(item, 'function'):
|
||||||
|
for name in item.function.__dict__:
|
||||||
|
mapped_names.add(name)
|
||||||
|
|
||||||
|
return cls(mapped_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.
|
||||||
|
"""
|
||||||
|
mapping = KeywordMapping.from_item(colitem)
|
||||||
|
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))
|
|
@ -1,17 +1,13 @@
|
||||||
""" 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 collections import namedtuple
|
||||||
|
import warnings
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from ..deprecated import MARK_PARAMETERSET_UNPACKING
|
||||||
|
from ..compat import NOTSET, getfslineno
|
||||||
from six.moves import map
|
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"
|
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||||
|
|
||||||
|
@ -26,6 +22,25 @@ def alias(name, warning=None):
|
||||||
return property(getter if warning is None else warned, doc='alias for ' + name)
|
return property(getter if warning is None else warned, doc='alias for ' + name)
|
||||||
|
|
||||||
|
|
||||||
|
def istestfunc(func):
|
||||||
|
return hasattr(func, "__call__") and \
|
||||||
|
getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||||
|
|
||||||
|
|
||||||
|
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 ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||||
@classmethod
|
@classmethod
|
||||||
def param(cls, *values, **kw):
|
def param(cls, *values, **kw):
|
||||||
|
@ -96,258 +111,6 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||||
return argnames, parameters
|
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):
|
|
||||||
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)
|
@attr.s(frozen=True)
|
||||||
class Mark(object):
|
class Mark(object):
|
||||||
name = attr.ib()
|
name = attr.ib()
|
||||||
|
@ -476,6 +239,33 @@ def store_legacy_markinfo(func, mark):
|
||||||
holder.add_mark(mark)
|
holder.add_mark(mark)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
class MarkInfo(object):
|
class MarkInfo(object):
|
||||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||||
|
|
||||||
|
@ -501,31 +291,40 @@ class MarkInfo(object):
|
||||||
return map(MarkInfo, self._marks)
|
return map(MarkInfo, self._marks)
|
||||||
|
|
||||||
|
|
||||||
|
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,))
|
||||||
|
|
||||||
|
|
||||||
MARK_GEN = MarkGenerator()
|
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)
|
|
|
@ -28,7 +28,7 @@ from _pytest.compat import (
|
||||||
safe_str, getlocation, enum,
|
safe_str, getlocation, enum,
|
||||||
)
|
)
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.mark import transfer_markers
|
from _pytest.mark.structures import transfer_markers
|
||||||
|
|
||||||
|
|
||||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
""" support for skip/xfail functions and markers. """
|
""" support for skip/xfail functions and markers. """
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
import os
|
|
||||||
import six
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.mark import MarkInfo, MarkDecorator
|
from _pytest.mark import MarkInfo, MarkDecorator
|
||||||
from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME
|
from _pytest.mark.evaluate import MarkEvaluator
|
||||||
|
from _pytest.outcomes import fail, skip, xfail
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
@ -60,112 +57,6 @@ def pytest_configure(config):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MarkEvaluator(object):
|
|
||||||
def __init__(self, item, name):
|
|
||||||
self.item = item
|
|
||||||
self._marks = None
|
|
||||||
self._mark = None
|
|
||||||
self._mark_name = name
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
self._marks = self._get_marks()
|
|
||||||
return bool(self._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 []
|
|
||||||
|
|
||||||
def invalidraise(self, exc):
|
|
||||||
raises = self.get('raises')
|
|
||||||
if not raises:
|
|
||||||
return
|
|
||||||
return not isinstance(exc, raises)
|
|
||||||
|
|
||||||
def istrue(self):
|
|
||||||
try:
|
|
||||||
return self._istrue()
|
|
||||||
except TEST_OUTCOME:
|
|
||||||
self.exc = sys.exc_info()
|
|
||||||
if isinstance(self.exc[1], SyntaxError):
|
|
||||||
msg = [" " * (self.exc[1].offset + 4) + "^", ]
|
|
||||||
msg.append("SyntaxError: invalid syntax")
|
|
||||||
else:
|
|
||||||
msg = traceback.format_exception_only(*self.exc[:2])
|
|
||||||
fail("Error evaluating %r expression\n"
|
|
||||||
" %s\n"
|
|
||||||
"%s"
|
|
||||||
% (self._mark_name, self.expr, "\n".join(msg)),
|
|
||||||
pytrace=False)
|
|
||||||
|
|
||||||
def _getglobals(self):
|
|
||||||
d = {'os': os, 'sys': sys, 'config': self.item.config}
|
|
||||||
if hasattr(self.item, 'obj'):
|
|
||||||
d.update(self.item.obj.__globals__)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def _istrue(self):
|
|
||||||
if hasattr(self, 'result'):
|
|
||||||
return self.result
|
|
||||||
self._marks = self._get_marks()
|
|
||||||
|
|
||||||
if self._marks:
|
|
||||||
self.result = False
|
|
||||||
for mark in self._marks:
|
|
||||||
self._mark = mark
|
|
||||||
if 'condition' in mark.kwargs:
|
|
||||||
args = (mark.kwargs['condition'],)
|
|
||||||
else:
|
|
||||||
args = mark.args
|
|
||||||
|
|
||||||
for expr in args:
|
|
||||||
self.expr = expr
|
|
||||||
if isinstance(expr, six.string_types):
|
|
||||||
d = self._getglobals()
|
|
||||||
result = cached_eval(self.item.config, expr, d)
|
|
||||||
else:
|
|
||||||
if "reason" not in mark.kwargs:
|
|
||||||
# XXX better be checked at collection time
|
|
||||||
msg = "you need to specify reason=STRING " \
|
|
||||||
"when using booleans as conditions."
|
|
||||||
fail(msg)
|
|
||||||
result = bool(expr)
|
|
||||||
if result:
|
|
||||||
self.result = True
|
|
||||||
self.reason = mark.kwargs.get('reason', None)
|
|
||||||
self.expr = expr
|
|
||||||
return self.result
|
|
||||||
|
|
||||||
if not args:
|
|
||||||
self.result = True
|
|
||||||
self.reason = mark.kwargs.get('reason', None)
|
|
||||||
return self.result
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get(self, attr, default=None):
|
|
||||||
if self._mark is None:
|
|
||||||
return default
|
|
||||||
return self._mark.kwargs.get(attr, default)
|
|
||||||
|
|
||||||
def getexplanation(self):
|
|
||||||
expl = getattr(self, 'reason', None) or self.get('reason', None)
|
|
||||||
if not expl:
|
|
||||||
if not hasattr(self, 'expr'):
|
|
||||||
return ""
|
|
||||||
else:
|
|
||||||
return "condition: " + str(self.expr)
|
|
||||||
return expl
|
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
# Check if skip or skipif are specified as pytest marks
|
# Check if skip or skipif are specified as pytest marks
|
||||||
|
@ -294,18 +185,8 @@ def pytest_terminal_summary(terminalreporter):
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for char in tr.reportchars:
|
for char in tr.reportchars:
|
||||||
if char == "x":
|
action = REPORTCHAR_ACTIONS.get(char, lambda tr, lines: None)
|
||||||
show_xfailed(terminalreporter, lines)
|
action(terminalreporter, lines)
|
||||||
elif char == "X":
|
|
||||||
show_xpassed(terminalreporter, lines)
|
|
||||||
elif char in "fF":
|
|
||||||
show_simple(terminalreporter, lines, 'failed', "FAIL %s")
|
|
||||||
elif char in "sS":
|
|
||||||
show_skipped(terminalreporter, lines)
|
|
||||||
elif char == "E":
|
|
||||||
show_simple(terminalreporter, lines, 'error', "ERROR %s")
|
|
||||||
elif char == 'p':
|
|
||||||
show_simple(terminalreporter, lines, 'passed', "PASSED %s")
|
|
||||||
|
|
||||||
if lines:
|
if lines:
|
||||||
tr._tw.sep("=", "short test summary info")
|
tr._tw.sep("=", "short test summary info")
|
||||||
|
@ -341,18 +222,6 @@ def show_xpassed(terminalreporter, lines):
|
||||||
lines.append("XPASS %s %s" % (pos, reason))
|
lines.append("XPASS %s %s" % (pos, reason))
|
||||||
|
|
||||||
|
|
||||||
def cached_eval(config, expr, d):
|
|
||||||
if not hasattr(config, '_evalcache'):
|
|
||||||
config._evalcache = {}
|
|
||||||
try:
|
|
||||||
return config._evalcache[expr]
|
|
||||||
except KeyError:
|
|
||||||
import _pytest._code
|
|
||||||
exprcode = _pytest._code.compile(expr, mode="eval")
|
|
||||||
config._evalcache[expr] = x = eval(exprcode, d)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def folded_skips(skipped):
|
def folded_skips(skipped):
|
||||||
d = {}
|
d = {}
|
||||||
for event in skipped:
|
for event in skipped:
|
||||||
|
@ -395,3 +264,22 @@ def show_skipped(terminalreporter, lines):
|
||||||
lines.append(
|
lines.append(
|
||||||
"SKIP [%d] %s: %s" %
|
"SKIP [%d] %s: %s" %
|
||||||
(num, fspath, reason))
|
(num, fspath, reason))
|
||||||
|
|
||||||
|
|
||||||
|
def shower(stat, format):
|
||||||
|
def show_(terminalreporter, lines):
|
||||||
|
return show_simple(terminalreporter, lines, stat, format)
|
||||||
|
return show_
|
||||||
|
|
||||||
|
|
||||||
|
REPORTCHAR_ACTIONS = {
|
||||||
|
'x': show_xfailed,
|
||||||
|
'X': show_xpassed,
|
||||||
|
'f': shower('failed', "FAIL %s"),
|
||||||
|
'F': shower('failed', "FAIL %s"),
|
||||||
|
's': show_skipped,
|
||||||
|
'S': show_skipped,
|
||||||
|
'p': shower('passed', "PASSED %s"),
|
||||||
|
'E': shower('error', "ERROR %s")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Internal ``mark.py`` module has been turned into a package.
|
2
setup.py
2
setup.py
|
@ -101,7 +101,7 @@ def main():
|
||||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
extras_require=extras_require,
|
extras_require=extras_require,
|
||||||
packages=['_pytest', '_pytest.assertion', '_pytest._code'],
|
packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.mark'],
|
||||||
py_modules=['pytest'],
|
py_modules=['pytest'],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue