stash: make Stash and StashKey public

It's had enough time to bake - let's allow external plugins to use it.
This commit is contained in:
Ran Benita 2021-07-18 14:05:58 +03:00
parent 5f39e31736
commit c6bdeb8491
7 changed files with 79 additions and 29 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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
----------------

View File

@ -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 = Stash()
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
self.hook.pytest_addoption.call_historic(

View File

@ -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 = Stash()
#: 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:

View File

@ -14,7 +14,7 @@ D = TypeVar("D")
class StashKey(Generic[T]):
"""``StashKey`` is an object used as a key to a ``Stash``.
"""``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.
@ -29,17 +29,19 @@ class Stash:
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``:
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 wants to store data in this Stash, it creates ``StashKey``\s
for its keys (at the module level):
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]()
@ -59,25 +61,6 @@ class Stash:
some_str = stash[some_str_key]
# The static type of some_bool is bool.
some_bool = stash[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 ``Stash`` to the object. Module
External mints StashKeys for its own keys. Module External stores and
retrieves its data using these keys.
"""
__slots__ = ("_storage",)

View File

@ -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",