diff --git a/_pytest/mark/__init__.py b/_pytest/mark/__init__.py new file mode 100644 index 000000000..f22db5820 --- /dev/null +++ b/_pytest/mark/__init__.py @@ -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) diff --git a/_pytest/mark/evaluate.py b/_pytest/mark/evaluate.py new file mode 100644 index 000000000..f84d7455d --- /dev/null +++ b/_pytest/mark/evaluate.py @@ -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 diff --git a/_pytest/mark/legacy.py b/_pytest/mark/legacy.py new file mode 100644 index 000000000..ec45f12af --- /dev/null +++ b/_pytest/mark/legacy.py @@ -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)) diff --git a/_pytest/mark.py b/_pytest/mark/structures.py similarity index 60% rename from _pytest/mark.py rename to _pytest/mark/structures.py index 3cac9dc91..5550dc546 100644 --- a/_pytest/mark.py +++ b/_pytest/mark/structures.py @@ -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 +import warnings 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 _pytest.config import UsageError -from .deprecated import MARK_PARAMETERSET_UNPACKING -from .compat import NOTSET, getfslineno 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) +def istestfunc(func): + return hasattr(func, "__call__") and \ + getattr(func, "__name__", "") != "" + + +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')): @classmethod def param(cls, *values, **kw): @@ -96,258 +111,6 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): 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__", "") != "" - - @attr.s(frozen=True) class Mark(object): name = attr.ib() @@ -476,6 +239,33 @@ def store_legacy_markinfo(func, 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): """ Marking object created by :class:`MarkDecorator` instances. """ @@ -501,31 +291,40 @@ class MarkInfo(object): 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() - - -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) diff --git a/_pytest/python.py b/_pytest/python.py index fb7bac8b8..cdcfed49b 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -28,7 +28,7 @@ from _pytest.compat import ( safe_str, getlocation, enum, ) 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; diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 98fc51c7f..1a4187c1b 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -1,14 +1,11 @@ """ support for skip/xfail functions and markers. """ from __future__ import absolute_import, division, print_function -import os -import six -import sys -import traceback from _pytest.config import hookimpl 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): @@ -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) def pytest_runtest_setup(item): # Check if skip or skipif are specified as pytest marks @@ -294,18 +185,8 @@ def pytest_terminal_summary(terminalreporter): lines = [] for char in tr.reportchars: - if char == "x": - show_xfailed(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") + action = REPORTCHAR_ACTIONS.get(char, lambda tr, lines: None) + action(terminalreporter, lines) if lines: tr._tw.sep("=", "short test summary info") @@ -341,18 +222,6 @@ def show_xpassed(terminalreporter, lines): 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): d = {} for event in skipped: @@ -395,3 +264,22 @@ def show_skipped(terminalreporter, lines): lines.append( "SKIP [%d] %s: %s" % (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") + +} diff --git a/changelog/3250.trivial.rst b/changelog/3250.trivial.rst new file mode 100644 index 000000000..a80bac513 --- /dev/null +++ b/changelog/3250.trivial.rst @@ -0,0 +1 @@ +Internal ``mark.py`` module has been turned into a package. diff --git a/setup.py b/setup.py index e2013f3fc..78b3ebc5e 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,7 @@ def main(): python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', install_requires=install_requires, extras_require=extras_require, - packages=['_pytest', '_pytest.assertion', '_pytest._code'], + packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.mark'], py_modules=['pytest'], zip_safe=False, )