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 cf40c0743c
.
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.
This commit is contained in:
parent
a1f841d5d2
commit
6072c9950d
|
@ -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
|
|
|
@ -1,25 +1,28 @@
|
||||||
""" support for skip/xfail functions and markers. """
|
""" 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 Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
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.nodes import Item
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
from _pytest.outcomes import xfail
|
from _pytest.outcomes import xfail
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.store import StoreKey
|
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:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
group = parser.getgroup("general")
|
group = parser.getgroup("general")
|
||||||
group.addoption(
|
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)
|
@hookimpl(tryfirst=True)
|
||||||
def pytest_runtest_setup(item: Item) -> None:
|
def pytest_runtest_setup(item: Item) -> None:
|
||||||
# Check if skip or skipif are specified as pytest marks
|
# Check if skip or skipif are specified as pytest marks
|
||||||
|
|
Loading…
Reference in New Issue