From 6072c9950d76a20da5397547932557842b84e078 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 19 Jun 2020 13:33:54 +0300 Subject: [PATCH] skipping: move MarkEvaluator from _pytest.mark.evaluate to _pytest.skipping This type was actually in `_pytest.skipping` previously, but was moved to `_pytest.mark.evaluate` in cf40c0743c565ed25bc14753e2350e010b39025a. I think the previous location was more appropriate, because the `MarkEvaluator` is not a generic mark facility, it is explicitly and exclusively used by the `skipif` and `xfail` marks to evaluate their particular set of arguments. So it is better to put it in the plugin code. Putting `skipping` related functionality into the core `_pytest.mark` module also causes some import cycles which we can avoid. --- src/_pytest/mark/evaluate.py | 124 --------------------------------- src/_pytest/skipping.py | 131 +++++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 130 deletions(-) delete mode 100644 src/_pytest/mark/evaluate.py diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.py deleted file mode 100644 index eb9903a59..000000000 --- a/src/_pytest/mark/evaluate.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import platform -import sys -import traceback -from typing import Any -from typing import Dict -from typing import List -from typing import Optional - -from ..outcomes import fail -from ..outcomes import TEST_OUTCOME -from .structures import Mark -from _pytest.nodes import Item - - -def compiled_eval(expr: str, d: Dict[str, object]) -> Any: - import _pytest._code - - exprcode = _pytest._code.compile(expr, mode="eval") - return eval(exprcode, d) - - -class MarkEvaluator: - def __init__(self, item: Item, name: str) -> None: - self.item = item - self._marks = None # type: Optional[List[Mark]] - self._mark = None # type: Optional[Mark] - self._mark_name = name - - def __bool__(self) -> bool: - # don't cache here to prevent staleness - return bool(self._get_marks()) - - def wasvalid(self) -> bool: - return not hasattr(self, "exc") - - def _get_marks(self) -> List[Mark]: - return list(self.item.iter_markers(name=self._mark_name)) - - def invalidraise(self, exc) -> Optional[bool]: - raises = self.get("raises") - if not raises: - return None - return not isinstance(exc, raises) - - def istrue(self) -> bool: - try: - return self._istrue() - except TEST_OUTCOME: - self.exc = sys.exc_info() - if isinstance(self.exc[1], SyntaxError): - # TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here. - assert self.exc[1].offset is not None - 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) -> Dict[str, object]: - d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} - if hasattr(self.item, "obj"): - d.update(self.item.obj.__globals__) # type: ignore[attr-defined] # noqa: F821 - return d - - def _istrue(self) -> bool: - if hasattr(self, "result"): - result = getattr(self, "result") # type: bool - return result - self._marks = self._get_marks() - - if self._marks: - self.result = False - for mark in self._marks: - self._mark = mark - if "condition" not in mark.kwargs: - args = mark.args - else: - args = (mark.kwargs["condition"],) - - for expr in args: - self.expr = expr - if isinstance(expr, str): - d = self._getglobals() - result = compiled_eval(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/src/_pytest/skipping.py b/src/_pytest/skipping.py index 4e4b5a3c4..ee6b40daa 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,25 +1,28 @@ """ support for skip/xfail functions and markers. """ +import os +import platform +import sys +import traceback +from typing import Any +from typing import Dict +from typing import List from typing import Optional from typing import Tuple from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser -from _pytest.mark.evaluate import MarkEvaluator +from _pytest.mark.structures import Mark from _pytest.nodes import Item from _pytest.outcomes import fail from _pytest.outcomes import skip +from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import xfail from _pytest.reports import BaseReport from _pytest.runner import CallInfo from _pytest.store import StoreKey -skipped_by_mark_key = StoreKey[bool]() -evalxfail_key = StoreKey[MarkEvaluator]() -unexpectedsuccess_key = StoreKey[str]() - - def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( @@ -79,6 +82,122 @@ def pytest_configure(config: Config) -> None: ) +def compiled_eval(expr: str, d: Dict[str, object]) -> Any: + import _pytest._code + + exprcode = _pytest._code.compile(expr, mode="eval") + return eval(exprcode, d) + + +class MarkEvaluator: + def __init__(self, item: Item, name: str) -> None: + self.item = item + self._marks = None # type: Optional[List[Mark]] + self._mark = None # type: Optional[Mark] + self._mark_name = name + + def __bool__(self) -> bool: + # don't cache here to prevent staleness + return bool(self._get_marks()) + + def wasvalid(self) -> bool: + return not hasattr(self, "exc") + + def _get_marks(self) -> List[Mark]: + return list(self.item.iter_markers(name=self._mark_name)) + + def invalidraise(self, exc) -> Optional[bool]: + raises = self.get("raises") + if not raises: + return None + return not isinstance(exc, raises) + + def istrue(self) -> bool: + try: + return self._istrue() + except TEST_OUTCOME: + self.exc = sys.exc_info() + if isinstance(self.exc[1], SyntaxError): + # TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here. + assert self.exc[1].offset is not None + 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) -> Dict[str, object]: + d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} + if hasattr(self.item, "obj"): + d.update(self.item.obj.__globals__) # type: ignore[attr-defined] # noqa: F821 + return d + + def _istrue(self) -> bool: + if hasattr(self, "result"): + result = getattr(self, "result") # type: bool + return result + self._marks = self._get_marks() + + if self._marks: + self.result = False + for mark in self._marks: + self._mark = mark + if "condition" not in mark.kwargs: + args = mark.args + else: + args = (mark.kwargs["condition"],) + + for expr in args: + self.expr = expr + if isinstance(expr, str): + d = self._getglobals() + result = compiled_eval(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 + + +skipped_by_mark_key = StoreKey[bool]() +evalxfail_key = StoreKey[MarkEvaluator]() +unexpectedsuccess_key = StoreKey[str]() + + @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: # Check if skip or skipif are specified as pytest marks