Skip to content

Commit 6247a95

Browse files
authored
Merge pull request #8920 from bluetech/stabilize-store
Rename Store to Stash and make it public
2 parents 60d9891 + 2aaea20 commit 6247a95

21 files changed

+319
-258
lines changed

changelog/8920.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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.
2+
See :ref:`plugin-stash` for details.

doc/en/how-to/writing_hook_functions.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,42 @@ declaring the hook functions directly in your plugin module, for example:
311311
312312
This has the added benefit of allowing you to conditionally install hooks
313313
depending on which plugins are installed.
314+
315+
.. _plugin-stash:
316+
317+
Storing data on items across hook functions
318+
-------------------------------------------
319+
320+
Plugins often need to store data on :class:`~pytest.Item`\s in one hook
321+
implementation, and access it in another. One common solution is to just
322+
assign some private attribute directly on the item, but type-checkers like
323+
mypy frown upon this, and it may also cause conflicts with other plugins.
324+
So pytest offers a better way to do this, :attr:`_pytest.nodes.Node.stash <item.stash>`.
325+
326+
To use the "stash" in your plugins, first create "stash keys" somewhere at the
327+
top level of your plugin:
328+
329+
.. code-block:: python
330+
331+
been_there_key: pytest.StashKey[bool]()
332+
done_that_key: pytest.StashKey[str]()
333+
334+
then use the keys to stash your data at some point:
335+
336+
.. code-block:: python
337+
338+
def pytest_runtest_setup(item: pytest.Item) -> None:
339+
item.stash[been_there_key] = True
340+
item.stash[done_that_key] = "no"
341+
342+
and retrieve them at another point:
343+
344+
.. code-block:: python
345+
346+
def pytest_runtest_teardown(item: pytest.Item) -> None:
347+
if not item.stash[been_there_key]:
348+
print("Oh?")
349+
item.stash[done_that_key] = "yes!"
350+
351+
Stashes are available on all node types (like :class:`~pytest.Class`,
352+
:class:`~pytest.Session`) and also on :class:`~pytest.Config`, if needed.

doc/en/reference/reference.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,18 @@ Result used within :ref:`hook wrappers <hookwrapper>`.
962962
.. automethod:: pluggy.callers._Result.get_result
963963
.. automethod:: pluggy.callers._Result.force_result
964964

965+
Stash
966+
~~~~~
967+
968+
.. autoclass:: pytest.Stash
969+
:special-members: __setitem__, __getitem__, __delitem__, __contains__, __len__
970+
:members:
971+
972+
.. autoclass:: pytest.StashKey
973+
:show-inheritance:
974+
:members:
975+
976+
965977
Global Variables
966978
----------------
967979

src/_pytest/assertion/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ def __init__(self, config: Config, mode) -> None:
8888

8989
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
9090
"""Try to install the rewrite hook, raise SystemError if it fails."""
91-
config._store[assertstate_key] = AssertionState(config, "rewrite")
92-
config._store[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
91+
config.stash[assertstate_key] = AssertionState(config, "rewrite")
92+
config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
9393
sys.meta_path.insert(0, hook)
94-
config._store[assertstate_key].trace("installed rewrite import hook")
94+
config.stash[assertstate_key].trace("installed rewrite import hook")
9595

9696
def undo() -> None:
97-
hook = config._store[assertstate_key].hook
97+
hook = config.stash[assertstate_key].hook
9898
if hook is not None and hook in sys.meta_path:
9999
sys.meta_path.remove(hook)
100100

@@ -106,7 +106,7 @@ def pytest_collection(session: "Session") -> None:
106106
# This hook is only called when test modules are collected
107107
# so for example not in the managing process of pytest-xdist
108108
# (which does not collect test modules).
109-
assertstate = session.config._store.get(assertstate_key, None)
109+
assertstate = session.config.stash.get(assertstate_key, None)
110110
if assertstate:
111111
if assertstate.hook is not None:
112112
assertstate.hook.set_session(session)
@@ -169,7 +169,7 @@ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
169169

170170

171171
def pytest_sessionfinish(session: "Session") -> None:
172-
assertstate = session.config._store.get(assertstate_key, None)
172+
assertstate = session.config.stash.get(assertstate_key, None)
173173
if assertstate:
174174
if assertstate.hook is not None:
175175
assertstate.hook.set_session(None)

src/_pytest/assertion/rewrite.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@
3838
from _pytest.main import Session
3939
from _pytest.pathlib import absolutepath
4040
from _pytest.pathlib import fnmatch_ex
41-
from _pytest.store import StoreKey
41+
from _pytest.stash import StashKey
4242

4343
if TYPE_CHECKING:
4444
from _pytest.assertion import AssertionState
4545

4646

47-
assertstate_key = StoreKey["AssertionState"]()
47+
assertstate_key = StashKey["AssertionState"]()
4848

4949

5050
# pytest caches rewritten pycs in pycache dirs
@@ -87,7 +87,7 @@ def find_spec(
8787
) -> Optional[importlib.machinery.ModuleSpec]:
8888
if self._writing_pyc:
8989
return None
90-
state = self.config._store[assertstate_key]
90+
state = self.config.stash[assertstate_key]
9191
if self._early_rewrite_bailout(name, state):
9292
return None
9393
state.trace("find_module called for: %s" % name)
@@ -131,7 +131,7 @@ def exec_module(self, module: types.ModuleType) -> None:
131131
assert module.__spec__ is not None
132132
assert module.__spec__.origin is not None
133133
fn = Path(module.__spec__.origin)
134-
state = self.config._store[assertstate_key]
134+
state = self.config.stash[assertstate_key]
135135

136136
self._rewritten_names.add(module.__name__)
137137

src/_pytest/config/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
from _pytest.pathlib import import_path
5757
from _pytest.pathlib import ImportMode
5858
from _pytest.pathlib import resolve_package_path
59-
from _pytest.store import Store
59+
from _pytest.stash import Stash
6060
from _pytest.warning_types import PytestConfigWarning
6161

6262
if TYPE_CHECKING:
@@ -923,6 +923,15 @@ def __init__(
923923
:type: PytestPluginManager
924924
"""
925925

926+
self.stash = Stash()
927+
"""A place where plugins can store information on the config for their
928+
own use.
929+
930+
:type: Stash
931+
"""
932+
# Deprecated alias. Was never public. Can be removed in a few releases.
933+
self._store = self.stash
934+
926935
from .compat import PathAwareHookProxy
927936

928937
self.trace = self.pluginmanager.trace.root.get("config")
@@ -931,9 +940,6 @@ def __init__(
931940
self._override_ini: Sequence[str] = ()
932941
self._opt2dest: Dict[str, str] = {}
933942
self._cleanup: List[Callable[[], None]] = []
934-
# A place where plugins can store information on the config for their
935-
# own use. Currently only intended for internal plugins.
936-
self._store = Store()
937943
self.pluginmanager.register(self, "pytestconfig")
938944
self._configured = False
939945
self.hook.pytest_addoption.call_historic(

src/_pytest/faulthandler.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from _pytest.config import Config
99
from _pytest.config.argparsing import Parser
1010
from _pytest.nodes import Item
11-
from _pytest.store import StoreKey
11+
from _pytest.stash import StashKey
1212

1313

14-
fault_handler_stderr_key = StoreKey[TextIO]()
15-
fault_handler_originally_enabled_key = StoreKey[bool]()
14+
fault_handler_stderr_key = StashKey[TextIO]()
15+
fault_handler_originally_enabled_key = StashKey[bool]()
1616

1717

1818
def pytest_addoption(parser: Parser) -> None:
@@ -27,20 +27,20 @@ def pytest_configure(config: Config) -> None:
2727
import faulthandler
2828

2929
stderr_fd_copy = os.dup(get_stderr_fileno())
30-
config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
31-
config._store[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
32-
faulthandler.enable(file=config._store[fault_handler_stderr_key])
30+
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
31+
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
32+
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
3333

3434

3535
def pytest_unconfigure(config: Config) -> None:
3636
import faulthandler
3737

3838
faulthandler.disable()
3939
# Close the dup file installed during pytest_configure.
40-
if fault_handler_stderr_key in config._store:
41-
config._store[fault_handler_stderr_key].close()
42-
del config._store[fault_handler_stderr_key]
43-
if config._store.get(fault_handler_originally_enabled_key, False):
40+
if fault_handler_stderr_key in config.stash:
41+
config.stash[fault_handler_stderr_key].close()
42+
del config.stash[fault_handler_stderr_key]
43+
if config.stash.get(fault_handler_originally_enabled_key, False):
4444
# Re-enable the faulthandler if it was originally enabled.
4545
faulthandler.enable(file=get_stderr_fileno())
4646

@@ -67,7 +67,7 @@ def get_timeout_config_value(config: Config) -> float:
6767
@pytest.hookimpl(hookwrapper=True, trylast=True)
6868
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
6969
timeout = get_timeout_config_value(item.config)
70-
stderr = item.config._store[fault_handler_stderr_key]
70+
stderr = item.config.stash[fault_handler_stderr_key]
7171
if timeout > 0 and stderr is not None:
7272
import faulthandler
7373

src/_pytest/fixtures.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from _pytest.outcomes import TEST_OUTCOME
6363
from _pytest.pathlib import absolutepath
6464
from _pytest.pathlib import bestrelpath
65-
from _pytest.store import StoreKey
65+
from _pytest.stash import StashKey
6666

6767
if TYPE_CHECKING:
6868
from typing import Deque
@@ -149,7 +149,7 @@ def get_scope_node(
149149

150150

151151
# Used for storing artificial fixturedefs for direct parametrization.
152-
name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]()
152+
name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
153153

154154

155155
def add_funcarg_pseudo_fixture_def(
@@ -199,7 +199,7 @@ def add_funcarg_pseudo_fixture_def(
199199
name2pseudofixturedef = None
200200
else:
201201
default: Dict[str, FixtureDef[Any]] = {}
202-
name2pseudofixturedef = node._store.setdefault(
202+
name2pseudofixturedef = node.stash.setdefault(
203203
name2pseudofixturedef_key, default
204204
)
205205
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:

src/_pytest/junitxml.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
from _pytest.config.argparsing import Parser
3131
from _pytest.fixtures import FixtureRequest
3232
from _pytest.reports import TestReport
33-
from _pytest.store import StoreKey
33+
from _pytest.stash import StashKey
3434
from _pytest.terminal import TerminalReporter
3535

3636

37-
xml_key = StoreKey["LogXML"]()
37+
xml_key = StashKey["LogXML"]()
3838

3939

4040
def bin_xml_escape(arg: object) -> str:
@@ -267,7 +267,7 @@ def _warn_incompatibility_with_xunit2(
267267
"""Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
268268
from _pytest.warning_types import PytestWarning
269269

270-
xml = request.config._store.get(xml_key, None)
270+
xml = request.config.stash.get(xml_key, None)
271271
if xml is not None and xml.family not in ("xunit1", "legacy"):
272272
request.node.warn(
273273
PytestWarning(
@@ -322,7 +322,7 @@ def add_attr_noop(name: str, value: object) -> None:
322322

323323
attr_func = add_attr_noop
324324

325-
xml = request.config._store.get(xml_key, None)
325+
xml = request.config.stash.get(xml_key, None)
326326
if xml is not None:
327327
node_reporter = xml.node_reporter(request.node.nodeid)
328328
attr_func = node_reporter.add_attribute
@@ -370,7 +370,7 @@ def record_func(name: str, value: object) -> None:
370370
__tracebackhide__ = True
371371
_check_record_param_type("name", name)
372372

373-
xml = request.config._store.get(xml_key, None)
373+
xml = request.config.stash.get(xml_key, None)
374374
if xml is not None:
375375
record_func = xml.add_global_property # noqa
376376
return record_func
@@ -428,7 +428,7 @@ def pytest_configure(config: Config) -> None:
428428
# Prevent opening xmllog on worker nodes (xdist).
429429
if xmlpath and not hasattr(config, "workerinput"):
430430
junit_family = config.getini("junit_family")
431-
config._store[xml_key] = LogXML(
431+
config.stash[xml_key] = LogXML(
432432
xmlpath,
433433
config.option.junitprefix,
434434
config.getini("junit_suite_name"),
@@ -437,13 +437,13 @@ def pytest_configure(config: Config) -> None:
437437
junit_family,
438438
config.getini("junit_log_passing_tests"),
439439
)
440-
config.pluginmanager.register(config._store[xml_key])
440+
config.pluginmanager.register(config.stash[xml_key])
441441

442442

443443
def pytest_unconfigure(config: Config) -> None:
444-
xml = config._store.get(xml_key, None)
444+
xml = config.stash.get(xml_key, None)
445445
if xml:
446-
del config._store[xml_key]
446+
del config.stash[xml_key]
447447
config.pluginmanager.unregister(xml)
448448

449449

src/_pytest/logging.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@
3131
from _pytest.fixtures import fixture
3232
from _pytest.fixtures import FixtureRequest
3333
from _pytest.main import Session
34-
from _pytest.store import StoreKey
34+
from _pytest.stash import StashKey
3535
from _pytest.terminal import TerminalReporter
3636

3737

3838
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
3939
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
4040
_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
41-
caplog_handler_key = StoreKey["LogCaptureHandler"]()
42-
caplog_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]()
41+
caplog_handler_key = StashKey["LogCaptureHandler"]()
42+
caplog_records_key = StashKey[Dict[str, List[logging.LogRecord]]]()
4343

4444

4545
def _remove_ansi_escape_sequences(text: str) -> str:
@@ -372,7 +372,7 @@ def handler(self) -> LogCaptureHandler:
372372
373373
:rtype: LogCaptureHandler
374374
"""
375-
return self._item._store[caplog_handler_key]
375+
return self._item.stash[caplog_handler_key]
376376

377377
def get_records(self, when: str) -> List[logging.LogRecord]:
378378
"""Get the logging records for one of the possible test phases.
@@ -385,7 +385,7 @@ def get_records(self, when: str) -> List[logging.LogRecord]:
385385
386386
.. versionadded:: 3.4
387387
"""
388-
return self._item._store[caplog_records_key].get(when, [])
388+
return self._item.stash[caplog_records_key].get(when, [])
389389

390390
@property
391391
def text(self) -> str:
@@ -694,8 +694,8 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non
694694
) as report_handler:
695695
caplog_handler.reset()
696696
report_handler.reset()
697-
item._store[caplog_records_key][when] = caplog_handler.records
698-
item._store[caplog_handler_key] = caplog_handler
697+
item.stash[caplog_records_key][when] = caplog_handler.records
698+
item.stash[caplog_handler_key] = caplog_handler
699699

700700
yield
701701

@@ -707,7 +707,7 @@ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
707707
self.log_cli_handler.set_when("setup")
708708

709709
empty: Dict[str, List[logging.LogRecord]] = {}
710-
item._store[caplog_records_key] = empty
710+
item.stash[caplog_records_key] = empty
711711
yield from self._runtest_for(item, "setup")
712712

713713
@hookimpl(hookwrapper=True)
@@ -721,8 +721,8 @@ def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, Non
721721
self.log_cli_handler.set_when("teardown")
722722

723723
yield from self._runtest_for(item, "teardown")
724-
del item._store[caplog_records_key]
725-
del item._store[caplog_handler_key]
724+
del item.stash[caplog_records_key]
725+
del item.stash[caplog_handler_key]
726726

727727
@hookimpl
728728
def pytest_runtest_logfinish(self) -> None:

0 commit comments

Comments
 (0)