Merge pull request #8920 from bluetech/stabilize-store
Rename Store to Stash and make it public
This commit is contained in:
commit
6247a95601
|
@ -0,0 +1,2 @@
|
|||
Added :class:`pytest.Stash`, a facility for plugins to store their data on :class:`~pytest.Config` and :class:`~_pytest.nodes.Node`\s in a type-safe and conflict-free manner.
|
||||
See :ref:`plugin-stash` for details.
|
|
@ -311,3 +311,42 @@ declaring the hook functions directly in your plugin module, for example:
|
|||
|
||||
This has the added benefit of allowing you to conditionally install hooks
|
||||
depending on which plugins are installed.
|
||||
|
||||
.. _plugin-stash:
|
||||
|
||||
Storing data on items across hook functions
|
||||
-------------------------------------------
|
||||
|
||||
Plugins often need to store data on :class:`~pytest.Item`\s in one hook
|
||||
implementation, and access it in another. One common solution is to just
|
||||
assign some private attribute directly on the item, but type-checkers like
|
||||
mypy frown upon this, and it may also cause conflicts with other plugins.
|
||||
So pytest offers a better way to do this, :attr:`_pytest.nodes.Node.stash <item.stash>`.
|
||||
|
||||
To use the "stash" in your plugins, first create "stash keys" somewhere at the
|
||||
top level of your plugin:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
been_there_key: pytest.StashKey[bool]()
|
||||
done_that_key: pytest.StashKey[str]()
|
||||
|
||||
then use the keys to stash your data at some point:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_runtest_setup(item: pytest.Item) -> None:
|
||||
item.stash[been_there_key] = True
|
||||
item.stash[done_that_key] = "no"
|
||||
|
||||
and retrieve them at another point:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_runtest_teardown(item: pytest.Item) -> None:
|
||||
if not item.stash[been_there_key]:
|
||||
print("Oh?")
|
||||
item.stash[done_that_key] = "yes!"
|
||||
|
||||
Stashes are available on all node types (like :class:`~pytest.Class`,
|
||||
:class:`~pytest.Session`) and also on :class:`~pytest.Config`, if needed.
|
||||
|
|
|
@ -962,6 +962,18 @@ Result used within :ref:`hook wrappers <hookwrapper>`.
|
|||
.. automethod:: pluggy.callers._Result.get_result
|
||||
.. automethod:: pluggy.callers._Result.force_result
|
||||
|
||||
Stash
|
||||
~~~~~
|
||||
|
||||
.. autoclass:: pytest.Stash
|
||||
:special-members: __setitem__, __getitem__, __delitem__, __contains__, __len__
|
||||
:members:
|
||||
|
||||
.. autoclass:: pytest.StashKey
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
|
||||
Global Variables
|
||||
----------------
|
||||
|
||||
|
|
|
@ -88,13 +88,13 @@ class AssertionState:
|
|||
|
||||
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
|
||||
"""Try to install the rewrite hook, raise SystemError if it fails."""
|
||||
config._store[assertstate_key] = AssertionState(config, "rewrite")
|
||||
config._store[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
|
||||
config.stash[assertstate_key] = AssertionState(config, "rewrite")
|
||||
config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
|
||||
sys.meta_path.insert(0, hook)
|
||||
config._store[assertstate_key].trace("installed rewrite import hook")
|
||||
config.stash[assertstate_key].trace("installed rewrite import hook")
|
||||
|
||||
def undo() -> None:
|
||||
hook = config._store[assertstate_key].hook
|
||||
hook = config.stash[assertstate_key].hook
|
||||
if hook is not None and hook in sys.meta_path:
|
||||
sys.meta_path.remove(hook)
|
||||
|
||||
|
@ -106,7 +106,7 @@ def pytest_collection(session: "Session") -> None:
|
|||
# This hook is only called when test modules are collected
|
||||
# so for example not in the managing process of pytest-xdist
|
||||
# (which does not collect test modules).
|
||||
assertstate = session.config._store.get(assertstate_key, None)
|
||||
assertstate = session.config.stash.get(assertstate_key, None)
|
||||
if assertstate:
|
||||
if assertstate.hook is not None:
|
||||
assertstate.hook.set_session(session)
|
||||
|
@ -169,7 +169,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
|||
|
||||
|
||||
def pytest_sessionfinish(session: "Session") -> None:
|
||||
assertstate = session.config._store.get(assertstate_key, None)
|
||||
assertstate = session.config.stash.get(assertstate_key, None)
|
||||
if assertstate:
|
||||
if assertstate.hook is not None:
|
||||
assertstate.hook.set_session(None)
|
||||
|
|
|
@ -38,13 +38,13 @@ from _pytest.config import Config
|
|||
from _pytest.main import Session
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion import AssertionState
|
||||
|
||||
|
||||
assertstate_key = StoreKey["AssertionState"]()
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
|
||||
|
||||
# pytest caches rewritten pycs in pycache dirs
|
||||
|
@ -87,7 +87,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
|||
) -> Optional[importlib.machinery.ModuleSpec]:
|
||||
if self._writing_pyc:
|
||||
return None
|
||||
state = self.config._store[assertstate_key]
|
||||
state = self.config.stash[assertstate_key]
|
||||
if self._early_rewrite_bailout(name, state):
|
||||
return None
|
||||
state.trace("find_module called for: %s" % name)
|
||||
|
@ -131,7 +131,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
|||
assert module.__spec__ is not None
|
||||
assert module.__spec__.origin is not None
|
||||
fn = Path(module.__spec__.origin)
|
||||
state = self.config._store[assertstate_key]
|
||||
state = self.config.stash[assertstate_key]
|
||||
|
||||
self._rewritten_names.add(module.__name__)
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ from _pytest.pathlib import bestrelpath
|
|||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.store import Store
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -923,6 +923,15 @@ class Config:
|
|||
:type: PytestPluginManager
|
||||
"""
|
||||
|
||||
self.stash = Stash()
|
||||
"""A place where plugins can store information on the config for their
|
||||
own use.
|
||||
|
||||
:type: Stash
|
||||
"""
|
||||
# Deprecated alias. Was never public. Can be removed in a few releases.
|
||||
self._store = self.stash
|
||||
|
||||
from .compat import PathAwareHookProxy
|
||||
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
|
@ -931,9 +940,6 @@ class Config:
|
|||
self._override_ini: Sequence[str] = ()
|
||||
self._opt2dest: Dict[str, str] = {}
|
||||
self._cleanup: List[Callable[[], None]] = []
|
||||
# A place where plugins can store information on the config for their
|
||||
# own use. Currently only intended for internal plugins.
|
||||
self._store = Store()
|
||||
self.pluginmanager.register(self, "pytestconfig")
|
||||
self._configured = False
|
||||
self.hook.pytest_addoption.call_historic(
|
||||
|
|
|
@ -8,11 +8,11 @@ import pytest
|
|||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
fault_handler_stderr_key = StoreKey[TextIO]()
|
||||
fault_handler_originally_enabled_key = StoreKey[bool]()
|
||||
fault_handler_stderr_key = StashKey[TextIO]()
|
||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
|
@ -27,9 +27,9 @@ def pytest_configure(config: Config) -> None:
|
|||
import faulthandler
|
||||
|
||||
stderr_fd_copy = os.dup(get_stderr_fileno())
|
||||
config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
||||
config._store[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||
faulthandler.enable(file=config._store[fault_handler_stderr_key])
|
||||
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
|
||||
|
||||
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
|
@ -37,10 +37,10 @@ def pytest_unconfigure(config: Config) -> None:
|
|||
|
||||
faulthandler.disable()
|
||||
# Close the dup file installed during pytest_configure.
|
||||
if fault_handler_stderr_key in config._store:
|
||||
config._store[fault_handler_stderr_key].close()
|
||||
del config._store[fault_handler_stderr_key]
|
||||
if config._store.get(fault_handler_originally_enabled_key, False):
|
||||
if fault_handler_stderr_key in config.stash:
|
||||
config.stash[fault_handler_stderr_key].close()
|
||||
del config.stash[fault_handler_stderr_key]
|
||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
||||
# Re-enable the faulthandler if it was originally enabled.
|
||||
faulthandler.enable(file=get_stderr_fileno())
|
||||
|
||||
|
@ -67,7 +67,7 @@ def get_timeout_config_value(config: Config) -> float:
|
|||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
timeout = get_timeout_config_value(item.config)
|
||||
stderr = item.config._store[fault_handler_stderr_key]
|
||||
stderr = item.config.stash[fault_handler_stderr_key]
|
||||
if timeout > 0 and stderr is not None:
|
||||
import faulthandler
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ from _pytest.outcomes import fail
|
|||
from _pytest.outcomes import TEST_OUTCOME
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Deque
|
||||
|
@ -149,7 +149,7 @@ def get_scope_node(
|
|||
|
||||
|
||||
# Used for storing artificial fixturedefs for direct parametrization.
|
||||
name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]()
|
||||
name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
|
||||
|
||||
|
||||
def add_funcarg_pseudo_fixture_def(
|
||||
|
@ -199,7 +199,7 @@ def add_funcarg_pseudo_fixture_def(
|
|||
name2pseudofixturedef = None
|
||||
else:
|
||||
default: Dict[str, FixtureDef[Any]] = {}
|
||||
name2pseudofixturedef = node._store.setdefault(
|
||||
name2pseudofixturedef = node.stash.setdefault(
|
||||
name2pseudofixturedef_key, default
|
||||
)
|
||||
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
|
||||
|
|
|
@ -30,11 +30,11 @@ from _pytest.config import filename_arg
|
|||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
xml_key = StoreKey["LogXML"]()
|
||||
xml_key = StashKey["LogXML"]()
|
||||
|
||||
|
||||
def bin_xml_escape(arg: object) -> str:
|
||||
|
@ -267,7 +267,7 @@ def _warn_incompatibility_with_xunit2(
|
|||
"""Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
xml = request.config._store.get(xml_key, None)
|
||||
xml = request.config.stash.get(xml_key, None)
|
||||
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
||||
request.node.warn(
|
||||
PytestWarning(
|
||||
|
@ -322,7 +322,7 @@ def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], Non
|
|||
|
||||
attr_func = add_attr_noop
|
||||
|
||||
xml = request.config._store.get(xml_key, None)
|
||||
xml = request.config.stash.get(xml_key, None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
attr_func = node_reporter.add_attribute
|
||||
|
@ -370,7 +370,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
|||
__tracebackhide__ = True
|
||||
_check_record_param_type("name", name)
|
||||
|
||||
xml = request.config._store.get(xml_key, None)
|
||||
xml = request.config.stash.get(xml_key, None)
|
||||
if xml is not None:
|
||||
record_func = xml.add_global_property # noqa
|
||||
return record_func
|
||||
|
@ -428,7 +428,7 @@ def pytest_configure(config: Config) -> None:
|
|||
# Prevent opening xmllog on worker nodes (xdist).
|
||||
if xmlpath and not hasattr(config, "workerinput"):
|
||||
junit_family = config.getini("junit_family")
|
||||
config._store[xml_key] = LogXML(
|
||||
config.stash[xml_key] = LogXML(
|
||||
xmlpath,
|
||||
config.option.junitprefix,
|
||||
config.getini("junit_suite_name"),
|
||||
|
@ -437,13 +437,13 @@ def pytest_configure(config: Config) -> None:
|
|||
junit_family,
|
||||
config.getini("junit_log_passing_tests"),
|
||||
)
|
||||
config.pluginmanager.register(config._store[xml_key])
|
||||
config.pluginmanager.register(config.stash[xml_key])
|
||||
|
||||
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
xml = config._store.get(xml_key, None)
|
||||
xml = config.stash.get(xml_key, None)
|
||||
if xml:
|
||||
del config._store[xml_key]
|
||||
del config.stash[xml_key]
|
||||
config.pluginmanager.unregister(xml)
|
||||
|
||||
|
||||
|
|
|
@ -31,15 +31,15 @@ from _pytest.deprecated import check_ispytest
|
|||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
|
||||
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
|
||||
_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
|
||||
caplog_handler_key = StoreKey["LogCaptureHandler"]()
|
||||
caplog_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]()
|
||||
caplog_handler_key = StashKey["LogCaptureHandler"]()
|
||||
caplog_records_key = StashKey[Dict[str, List[logging.LogRecord]]]()
|
||||
|
||||
|
||||
def _remove_ansi_escape_sequences(text: str) -> str:
|
||||
|
@ -372,7 +372,7 @@ class LogCaptureFixture:
|
|||
|
||||
:rtype: LogCaptureHandler
|
||||
"""
|
||||
return self._item._store[caplog_handler_key]
|
||||
return self._item.stash[caplog_handler_key]
|
||||
|
||||
def get_records(self, when: str) -> List[logging.LogRecord]:
|
||||
"""Get the logging records for one of the possible test phases.
|
||||
|
@ -385,7 +385,7 @@ class LogCaptureFixture:
|
|||
|
||||
.. versionadded:: 3.4
|
||||
"""
|
||||
return self._item._store[caplog_records_key].get(when, [])
|
||||
return self._item.stash[caplog_records_key].get(when, [])
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
|
@ -694,8 +694,8 @@ class LoggingPlugin:
|
|||
) as report_handler:
|
||||
caplog_handler.reset()
|
||||
report_handler.reset()
|
||||
item._store[caplog_records_key][when] = caplog_handler.records
|
||||
item._store[caplog_handler_key] = caplog_handler
|
||||
item.stash[caplog_records_key][when] = caplog_handler.records
|
||||
item.stash[caplog_handler_key] = caplog_handler
|
||||
|
||||
yield
|
||||
|
||||
|
@ -707,7 +707,7 @@ class LoggingPlugin:
|
|||
self.log_cli_handler.set_when("setup")
|
||||
|
||||
empty: Dict[str, List[logging.LogRecord]] = {}
|
||||
item._store[caplog_records_key] = empty
|
||||
item.stash[caplog_records_key] = empty
|
||||
yield from self._runtest_for(item, "setup")
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
|
@ -721,8 +721,8 @@ class LoggingPlugin:
|
|||
self.log_cli_handler.set_when("teardown")
|
||||
|
||||
yield from self._runtest_for(item, "teardown")
|
||||
del item._store[caplog_records_key]
|
||||
del item._store[caplog_handler_key]
|
||||
del item.stash[caplog_records_key]
|
||||
del item.stash[caplog_handler_key]
|
||||
|
||||
@hookimpl
|
||||
def pytest_runtest_logfinish(self) -> None:
|
||||
|
|
|
@ -25,7 +25,7 @@ from _pytest.config import UsageError
|
|||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import MINUS_K_COLON
|
||||
from _pytest.deprecated import MINUS_K_DASH
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.nodes import Item
|
||||
|
@ -41,7 +41,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
old_mark_config_key = StoreKey[Optional[Config]]()
|
||||
old_mark_config_key = StashKey[Optional[Config]]()
|
||||
|
||||
|
||||
def param(
|
||||
|
@ -266,7 +266,7 @@ def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None:
|
|||
|
||||
|
||||
def pytest_configure(config: Config) -> None:
|
||||
config._store[old_mark_config_key] = MARK_GEN._config
|
||||
config.stash[old_mark_config_key] = MARK_GEN._config
|
||||
MARK_GEN._config = config
|
||||
|
||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
|
@ -279,4 +279,4 @@ def pytest_configure(config: Config) -> None:
|
|||
|
||||
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
MARK_GEN._config = config._store.get(old_mark_config_key, None)
|
||||
MARK_GEN._config = config.stash.get(old_mark_config_key, None)
|
||||
|
|
|
@ -34,7 +34,7 @@ from _pytest.mark.structures import NodeKeywords
|
|||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.store import Store
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -218,9 +218,13 @@ class Node(metaclass=NodeMeta):
|
|||
if self.name != "()":
|
||||
self._nodeid += "::" + self.name
|
||||
|
||||
# A place where plugins can store information on the node for their
|
||||
# own use. Currently only intended for internal plugins.
|
||||
self._store = Store()
|
||||
#: A place where plugins can store information on the node for their
|
||||
#: own use.
|
||||
#:
|
||||
#: :type: Stash
|
||||
self.stash = Stash()
|
||||
# Deprecated alias. Was never public. Can be removed in a few releases.
|
||||
self._store = self.stash
|
||||
|
||||
@property
|
||||
def fspath(self) -> LEGACY_PATH:
|
||||
|
|
|
@ -8,11 +8,11 @@ import pytest
|
|||
from _pytest.config import Config
|
||||
from _pytest.config import create_terminal_writer
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
|
||||
pastebinfile_key = StoreKey[IO[bytes]]()
|
||||
pastebinfile_key = StashKey[IO[bytes]]()
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
|
@ -37,26 +37,26 @@ def pytest_configure(config: Config) -> None:
|
|||
# when using pytest-xdist, for example.
|
||||
if tr is not None:
|
||||
# pastebin file will be UTF-8 encoded binary file.
|
||||
config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b")
|
||||
config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")
|
||||
oldwrite = tr._tw.write
|
||||
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
if isinstance(s, str):
|
||||
s = s.encode("utf-8")
|
||||
config._store[pastebinfile_key].write(s)
|
||||
config.stash[pastebinfile_key].write(s)
|
||||
|
||||
tr._tw.write = tee_write
|
||||
|
||||
|
||||
def pytest_unconfigure(config: Config) -> None:
|
||||
if pastebinfile_key in config._store:
|
||||
pastebinfile = config._store[pastebinfile_key]
|
||||
if pastebinfile_key in config.stash:
|
||||
pastebinfile = config.stash[pastebinfile_key]
|
||||
# Get terminal contents and delete file.
|
||||
pastebinfile.seek(0)
|
||||
sessionlog = pastebinfile.read()
|
||||
pastebinfile.close()
|
||||
del config._store[pastebinfile_key]
|
||||
del config.stash[pastebinfile_key]
|
||||
# Undo our patching in the terminal reporter.
|
||||
tr = config.pluginmanager.getplugin("terminalreporter")
|
||||
del tr._tw.__dict__["write"]
|
||||
|
|
|
@ -21,7 +21,7 @@ from _pytest.outcomes import skip
|
|||
from _pytest.outcomes import xfail
|
||||
from _pytest.reports import BaseReport
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
|
@ -228,7 +228,7 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]:
|
|||
|
||||
|
||||
# Saves the xfail mark evaluation. Can be refreshed during call if None.
|
||||
xfailed_key = StoreKey[Optional[Xfail]]()
|
||||
xfailed_key = StashKey[Optional[Xfail]]()
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
|
@ -237,16 +237,16 @@ def pytest_runtest_setup(item: Item) -> None:
|
|||
if skipped:
|
||||
raise skip.Exception(skipped.reason, _use_item_location=True)
|
||||
|
||||
item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item)
|
||||
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
|
||||
if xfailed and not item.config.option.runxfail and not xfailed.run:
|
||||
xfail("[NOTRUN] " + xfailed.reason)
|
||||
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
|
||||
xfailed = item._store.get(xfailed_key, None)
|
||||
xfailed = item.stash.get(xfailed_key, None)
|
||||
if xfailed is None:
|
||||
item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item)
|
||||
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
|
||||
|
||||
if xfailed and not item.config.option.runxfail and not xfailed.run:
|
||||
xfail("[NOTRUN] " + xfailed.reason)
|
||||
|
@ -254,16 +254,16 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
|
|||
yield
|
||||
|
||||
# The test run may have added an xfail mark dynamically.
|
||||
xfailed = item._store.get(xfailed_key, None)
|
||||
xfailed = item.stash.get(xfailed_key, None)
|
||||
if xfailed is None:
|
||||
item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item)
|
||||
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
|
||||
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
xfailed = item._store.get(xfailed_key, None)
|
||||
xfailed = item.stash.get(xfailed_key, None)
|
||||
if item.config.option.runxfail:
|
||||
pass # don't interfere
|
||||
elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
|
||||
__all__ = ["Stash", "StashKey"]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
class StashKey(Generic[T]):
|
||||
"""``StashKey`` is an object used as a key to a :class:`Stash`.
|
||||
|
||||
A ``StashKey`` is associated with the type ``T`` of the value of the key.
|
||||
|
||||
A ``StashKey`` is unique and cannot conflict with another key.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class Stash:
|
||||
r"""``Stash`` is a type-safe heterogeneous mutable mapping that
|
||||
allows keys and value types to be defined separately from
|
||||
where it (the ``Stash``) is created.
|
||||
|
||||
Usually you will be given an object which has a ``Stash``, for example
|
||||
:class:`~pytest.Config` or a :class:`~_pytest.nodes.Node`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
stash: Stash = some_object.stash
|
||||
|
||||
If a module or plugin wants to store data in this ``Stash``, it creates
|
||||
:class:`StashKey`\s for its keys (at the module level):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# At the top-level of the module
|
||||
some_str_key = StashKey[str]()
|
||||
some_bool_key = StashKey[bool]()
|
||||
|
||||
To store information:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Value type must match the key.
|
||||
stash[some_str_key] = "value"
|
||||
stash[some_bool_key] = True
|
||||
|
||||
To retrieve the information:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# The static type of some_str is str.
|
||||
some_str = stash[some_str_key]
|
||||
# The static type of some_bool is bool.
|
||||
some_bool = stash[some_bool_key]
|
||||
"""
|
||||
|
||||
__slots__ = ("_storage",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._storage: Dict[StashKey[Any], object] = {}
|
||||
|
||||
def __setitem__(self, key: StashKey[T], value: T) -> None:
|
||||
"""Set a value for key."""
|
||||
self._storage[key] = value
|
||||
|
||||
def __getitem__(self, key: StashKey[T]) -> T:
|
||||
"""Get the value for key.
|
||||
|
||||
Raises ``KeyError`` if the key wasn't set before.
|
||||
"""
|
||||
return cast(T, self._storage[key])
|
||||
|
||||
def get(self, key: StashKey[T], default: D) -> Union[T, D]:
|
||||
"""Get the value for key, or return default if the key wasn't set
|
||||
before."""
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def setdefault(self, key: StashKey[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: StashKey[T]) -> None:
|
||||
"""Delete the value for key.
|
||||
|
||||
Raises ``KeyError`` if the key wasn't set before.
|
||||
"""
|
||||
del self._storage[key]
|
||||
|
||||
def __contains__(self, key: StashKey[T]) -> bool:
|
||||
"""Return whether key was set."""
|
||||
return key in self._storage
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return how many items exist in the stash."""
|
||||
return len(self._storage)
|
|
@ -1,125 +0,0 @@
|
|||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
|
||||
__all__ = ["Store", "StoreKey"]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
class StoreKey(Generic[T]):
|
||||
"""StoreKey is an object used as a key to a Store.
|
||||
|
||||
A StoreKey is associated with the type T of the value of the key.
|
||||
|
||||
A StoreKey is unique and cannot conflict with another key.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class Store:
|
||||
"""Store is a type-safe heterogeneous mutable mapping that
|
||||
allows keys and value types to be defined separately from
|
||||
where it (the Store) is created.
|
||||
|
||||
Usually you will be given an object which has a ``Store``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
store: Store = some_object.store
|
||||
|
||||
If a module wants to store data in this Store, it creates StoreKeys
|
||||
for its keys (at the module level):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
some_str_key = StoreKey[str]()
|
||||
some_bool_key = StoreKey[bool]()
|
||||
|
||||
To store information:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Value type must match the key.
|
||||
store[some_str_key] = "value"
|
||||
store[some_bool_key] = True
|
||||
|
||||
To retrieve the information:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# The static type of some_str is str.
|
||||
some_str = store[some_str_key]
|
||||
# The static type of some_bool is bool.
|
||||
some_bool = store[some_bool_key]
|
||||
|
||||
Why use this?
|
||||
-------------
|
||||
|
||||
Problem: module Internal defines an object. Module External, which
|
||||
module Internal doesn't know about, receives the object and wants to
|
||||
attach information to it, to be retrieved later given the object.
|
||||
|
||||
Bad solution 1: Module External assigns private attributes directly on
|
||||
the object. This doesn't work well because the type checker doesn't
|
||||
know about these attributes and it complains about undefined attributes.
|
||||
|
||||
Bad solution 2: module Internal adds a ``Dict[str, Any]`` attribute to
|
||||
the object. Module External stores its data in private keys of this dict.
|
||||
This doesn't work well because retrieved values are untyped.
|
||||
|
||||
Good solution: module Internal adds a ``Store`` to the object. Module
|
||||
External mints StoreKeys for its own keys. Module External stores and
|
||||
retrieves its data using these keys.
|
||||
"""
|
||||
|
||||
__slots__ = ("_store",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._store: Dict[StoreKey[Any], object] = {}
|
||||
|
||||
def __setitem__(self, key: StoreKey[T], value: T) -> None:
|
||||
"""Set a value for key."""
|
||||
self._store[key] = value
|
||||
|
||||
def __getitem__(self, key: StoreKey[T]) -> T:
|
||||
"""Get the value for key.
|
||||
|
||||
Raises ``KeyError`` if the key wasn't set before.
|
||||
"""
|
||||
return cast(T, self._store[key])
|
||||
|
||||
def get(self, key: StoreKey[T], default: D) -> Union[T, D]:
|
||||
"""Get the value for key, or return default if the key wasn't set
|
||||
before."""
|
||||
try:
|
||||
return self[key]
|
||||
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.
|
||||
|
||||
Raises ``KeyError`` if the key wasn't set before.
|
||||
"""
|
||||
del self._store[key]
|
||||
|
||||
def __contains__(self, key: StoreKey[T]) -> bool:
|
||||
"""Return whether key was set."""
|
||||
return key in self._store
|
|
@ -55,6 +55,8 @@ from _pytest.recwarn import deprecated_call
|
|||
from _pytest.recwarn import WarningsRecorder
|
||||
from _pytest.recwarn import warns
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.tmpdir import TempdirFactory
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
|
@ -131,6 +133,8 @@ __all__ = [
|
|||
"Session",
|
||||
"set_trace",
|
||||
"skip",
|
||||
"Stash",
|
||||
"StashKey",
|
||||
"version_tuple",
|
||||
"TempPathFactory",
|
||||
"Testdir",
|
||||
|
|
|
@ -169,7 +169,7 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
|
|||
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
|
||||
|
||||
# This reaches into private API, don't use this type of thing in real tests!
|
||||
assert set(caplog._item._store[caplog_records_key]) == {"setup", "call"}
|
||||
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
|
||||
|
||||
|
||||
def test_ini_controls_global_log_level(pytester: Pytester) -> None:
|
||||
|
|
|
@ -21,7 +21,7 @@ from _pytest.pytester import Pytester
|
|||
from _pytest.pytester import RunResult
|
||||
from _pytest.reports import BaseReport
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.store import Store
|
||||
from _pytest.stash import Stash
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
@ -951,7 +951,7 @@ def test_dont_configure_on_workers(tmp_path: Path) -> None:
|
|||
def __init__(self):
|
||||
self.pluginmanager = self
|
||||
self.option = self
|
||||
self._store = Store()
|
||||
self.stash = Stash()
|
||||
|
||||
def getini(self, name):
|
||||
return "pytest"
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import pytest
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
def test_stash() -> None:
|
||||
stash = Stash()
|
||||
|
||||
assert len(stash) == 0
|
||||
assert not stash
|
||||
|
||||
key1 = StashKey[str]()
|
||||
key2 = StashKey[int]()
|
||||
|
||||
# Basic functionality - single key.
|
||||
assert key1 not in stash
|
||||
stash[key1] = "hello"
|
||||
assert key1 in stash
|
||||
assert stash[key1] == "hello"
|
||||
assert stash.get(key1, None) == "hello"
|
||||
stash[key1] = "world"
|
||||
assert stash[key1] == "world"
|
||||
# Has correct type (no mypy error).
|
||||
stash[key1] + "string"
|
||||
assert len(stash) == 1
|
||||
assert stash
|
||||
|
||||
# No interaction with another key.
|
||||
assert key2 not in stash
|
||||
assert stash.get(key2, None) is None
|
||||
with pytest.raises(KeyError):
|
||||
stash[key2]
|
||||
with pytest.raises(KeyError):
|
||||
del stash[key2]
|
||||
stash[key2] = 1
|
||||
assert stash[key2] == 1
|
||||
# Has correct type (no mypy error).
|
||||
stash[key2] + 20
|
||||
del stash[key1]
|
||||
with pytest.raises(KeyError):
|
||||
del stash[key1]
|
||||
with pytest.raises(KeyError):
|
||||
stash[key1]
|
||||
|
||||
# setdefault
|
||||
stash[key1] = "existing"
|
||||
assert stash.setdefault(key1, "default") == "existing"
|
||||
assert stash[key1] == "existing"
|
||||
key_setdefault = StashKey[bytes]()
|
||||
assert stash.setdefault(key_setdefault, b"default") == b"default"
|
||||
assert stash[key_setdefault] == b"default"
|
||||
assert len(stash) == 3
|
||||
assert stash
|
||||
|
||||
# Can't accidentally add attributes to stash object itself.
|
||||
with pytest.raises(AttributeError):
|
||||
stash.foo = "nope" # type: ignore[attr-defined]
|
||||
|
||||
# No interaction with anoter stash.
|
||||
stash2 = Stash()
|
||||
key3 = StashKey[int]()
|
||||
assert key2 not in stash2
|
||||
stash2[key2] = 100
|
||||
stash2[key3] = 200
|
||||
assert stash2[key2] + stash2[key3] == 300
|
||||
assert stash[key2] == 1
|
||||
assert key3 not in stash
|
|
@ -1,60 +0,0 @@
|
|||
import pytest
|
||||
from _pytest.store import Store
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
|
||||
def test_store() -> None:
|
||||
store = Store()
|
||||
|
||||
key1 = StoreKey[str]()
|
||||
key2 = StoreKey[int]()
|
||||
|
||||
# Basic functionality - single key.
|
||||
assert key1 not in store
|
||||
store[key1] = "hello"
|
||||
assert key1 in store
|
||||
assert store[key1] == "hello"
|
||||
assert store.get(key1, None) == "hello"
|
||||
store[key1] = "world"
|
||||
assert store[key1] == "world"
|
||||
# Has correct type (no mypy error).
|
||||
store[key1] + "string"
|
||||
|
||||
# No interaction with another key.
|
||||
assert key2 not in store
|
||||
assert store.get(key2, None) is None
|
||||
with pytest.raises(KeyError):
|
||||
store[key2]
|
||||
with pytest.raises(KeyError):
|
||||
del store[key2]
|
||||
store[key2] = 1
|
||||
assert store[key2] == 1
|
||||
# Has correct type (no mypy error).
|
||||
store[key2] + 20
|
||||
del store[key1]
|
||||
with pytest.raises(KeyError):
|
||||
del store[key1]
|
||||
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]
|
||||
|
||||
# No interaction with anoter store.
|
||||
store2 = Store()
|
||||
key3 = StoreKey[int]()
|
||||
assert key2 not in store2
|
||||
store2[key2] = 100
|
||||
store2[key3] = 200
|
||||
assert store2[key2] + store2[key3] == 300
|
||||
assert store[key2] == 1
|
||||
assert key3 not in store
|
Loading…
Reference in New Issue