From 678d65f051532bc798bdce539d9a550263fa79c0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 5 Mar 2020 22:13:16 +0200 Subject: [PATCH 1/3] Store AssertionState in config's store instead of attribute Part of moving away from ad-hoc attributes to using the config's store. --- src/_pytest/assertion/__init__.py | 13 +++++++------ src/_pytest/assertion/rewrite.py | 13 +++++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index cdb034703..0a57f6afa 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -7,6 +7,7 @@ from typing import Optional from _pytest.assertion import rewrite from _pytest.assertion import truncate from _pytest.assertion import util +from _pytest.assertion.rewrite import assertstate_key from _pytest.compat import TYPE_CHECKING from _pytest.config import hookimpl @@ -82,13 +83,13 @@ class AssertionState: def install_importhook(config): """Try to install the rewrite hook, raise SystemError if it fails.""" - config._assertstate = AssertionState(config, "rewrite") - config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config) + config._store[assertstate_key] = AssertionState(config, "rewrite") + config._store[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config) sys.meta_path.insert(0, hook) - config._assertstate.trace("installed rewrite import hook") + config._store[assertstate_key].trace("installed rewrite import hook") def undo(): - hook = config._assertstate.hook + hook = config._store[assertstate_key].hook if hook is not None and hook in sys.meta_path: sys.meta_path.remove(hook) @@ -100,7 +101,7 @@ def pytest_collection(session: "Session") -> None: # this hook is only called when test modules are collected # so for example not in the master process of pytest-xdist # (which does not collect test modules) - assertstate = getattr(session.config, "_assertstate", None) + assertstate = session.config._store.get(assertstate_key, None) if assertstate: if assertstate.hook is not None: assertstate.hook.set_session(session) @@ -163,7 +164,7 @@ def pytest_runtest_protocol(item): def pytest_sessionfinish(session): - assertstate = getattr(session.config, "_assertstate", None) + assertstate = session.config._store.get(assertstate_key, None) if assertstate: if assertstate.hook is not None: assertstate.hook.set_session(None) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 00bdcfc3e..f84127dca 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -26,9 +26,18 @@ from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, ) from _pytest.compat import fspath +from _pytest.compat import TYPE_CHECKING from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import Path from _pytest.pathlib import PurePath +from _pytest.store import StoreKey + +if TYPE_CHECKING: + from _pytest.assertion import AssertionState # noqa: F401 + + +assertstate_key = StoreKey["AssertionState"]() + # pytest caches rewritten pycs in pycache dirs PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) @@ -65,7 +74,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) def find_spec(self, name, path=None, target=None): if self._writing_pyc: return None - state = self.config._assertstate + state = self.config._store[assertstate_key] if self._early_rewrite_bailout(name, state): return None state.trace("find_module called for: %s" % name) @@ -104,7 +113,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) def exec_module(self, module): fn = Path(module.__spec__.origin) - state = self.config._assertstate + state = self.config._store[assertstate_key] self._rewritten_names.add(module.__name__) From b1d7a187f29de08152c174e1468c8b5743c96dd3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 5 Mar 2020 23:24:01 +0200 Subject: [PATCH 2/3] Add setdefault() method to Store Can be useful in some cases. --- src/_pytest/store.py | 9 +++++++++ testing/test_store.py | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/_pytest/store.py b/src/_pytest/store.py index 0dcea1b93..eed50d103 100644 --- a/src/_pytest/store.py +++ b/src/_pytest/store.py @@ -104,6 +104,15 @@ class Store: except KeyError: return default + def setdefault(self, key: StoreKey[T], default: T) -> T: + """Return the value of key if already set, otherwise set the value + of key to default and return default.""" + try: + return self[key] + except KeyError: + self[key] = default + return default + def __delitem__(self, key: StoreKey[T]) -> None: """Delete the value for key. diff --git a/testing/test_store.py b/testing/test_store.py index ae6cfccdd..98014887e 100644 --- a/testing/test_store.py +++ b/testing/test_store.py @@ -37,6 +37,14 @@ def test_store() -> None: with pytest.raises(KeyError): store[key1] + # setdefault + store[key1] = "existing" + assert store.setdefault(key1, "default") == "existing" + assert store[key1] == "existing" + key_setdefault = StoreKey[bytes]() + assert store.setdefault(key_setdefault, b"default") == b"default" + assert store[key_setdefault] == b"default" + # Can't accidentally add attributes to store object itself. with pytest.raises(AttributeError): store.foo = "nope" # type: ignore[attr-defined] # noqa: F821 From f011bc642c2c42774b0d0893325649075d700f57 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 5 Mar 2020 23:25:04 +0200 Subject: [PATCH 3/3] Store mark's evalcache in config's store instead of attribute Part of moving away from ad-hoc attributes to using the config's store. --- src/_pytest/mark/evaluate.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.py index 53822e98f..772baf31b 100644 --- a/src/_pytest/mark/evaluate.py +++ b/src/_pytest/mark/evaluate.py @@ -2,21 +2,28 @@ import os import platform import sys import traceback +from typing import Any +from typing import Dict from ..outcomes import fail from ..outcomes import TEST_OUTCOME +from _pytest.config import Config +from _pytest.store import StoreKey -def cached_eval(config, expr, d): - if not hasattr(config, "_evalcache"): - config._evalcache = {} +evalcache_key = StoreKey[Dict[str, Any]]() + + +def cached_eval(config: Config, expr: str, d: Dict[str, object]) -> Any: + default = {} # type: Dict[str, object] + evalcache = config._store.setdefault(evalcache_key, default) try: - return config._evalcache[expr] + return evalcache[expr] except KeyError: import _pytest._code exprcode = _pytest._code.compile(expr, mode="eval") - config._evalcache[expr] = x = eval(exprcode, d) + evalcache[expr] = x = eval(exprcode, d) return x