014ReviewseniorPython / pytest真实来源改编 pytest

pytest:缓存 skipif/xfail 字符串条件的求值

审查一个 pytest 收集性能优化 PR:作者为 skipif/xfail 的字符串条件引入求值缓存,避免同一条件被反复编译和求值。

ai-pr.diffdiff · 83 lines
diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.pyindex 69a2ef2a3..759191668 100644--- a/src/_pytest/mark/evaluate.py+++ b/src/_pytest/mark/evaluate.py@@ -8,6 +8,30 @@ from typing import Dict  from ..outcomes import fail from ..outcomes import TEST_OUTCOME+from _pytest.config import Config+from _pytest.store import StoreKey+++evalcache_key = StoreKey[Dict[str, Any]]()+++def cached_eval(config: Config, expr: str, d: Dict[str, object]) -> Any:+    """Evaluate a condition string at most once per test session.++    Large suites attach the same skipif/xfail condition string to hundreds+    of tests; compiling and evaluating it again for every item is wasted+    work during collection. The result for each expression is stored on the+    session config and reused on later hits.+    """+    default = {}  # type: Dict[str, object]+    evalcache = config._store.setdefault(evalcache_key, default)+    try:+        return evalcache[expr]+    except KeyError:+        import _pytest._code++        exprcode = _pytest._code.compile(expr, mode="eval")+        evalcache[expr] = x = eval(exprcode, d)+        return x   class MarkEvaluator:@@ -70,10 +94,7 @@ class MarkEvaluator:                 for expr in args:                     self.expr = expr                     if isinstance(expr, str):                         d = self._getglobals()-                        import _pytest._code--                        exprcode = _pytest._code.compile(expr, mode="eval")-                        result = eval(exprcode, d)+                        result = cached_eval(self.item.config, expr, d)                     else:                         if "reason" not in mark.kwargs:                             # XXX better be checked at collection timediff --git a/testing/test_skipping.py b/testing/test_skipping.pyindex 8b3fae0d6..1c5b0c334 100644--- a/testing/test_skipping.py+++ b/testing/test_skipping.py@@ -117,6 +117,27 @@ class TestEvaluator:         assert ev.istrue()         expl = ev.getexplanation()         assert expl == "condition: config._hackxyz" +    def test_skipif_string_condition_cached(self, testdir):+        # Two tests carrying the same condition string: the second+        # evaluation is served from the cache and both are still skipped.+        testdir.makepyfile(+            """+            import pytest++            skip_all = True++            @pytest.mark.skipif("skip_all")+            def test_one():+                assert False++            @pytest.mark.skipif("skip_all")+            def test_two():+                assert False+            """+        )+        result = testdir.runpytest()+        result.assert_outcomes(skipped=2)+  class TestXFail:     @pytest.mark.parametrize("strict", [True, False])