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. """
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue