From c6bdeb84914d54a1dea04b79492150aa3d1b42f2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 18 Jul 2021 14:05:58 +0300 Subject: [PATCH] stash: make Stash and StashKey public It's had enough time to bake - let's allow external plugins to use it. --- changelog/8920.feature.rst | 2 ++ doc/en/how-to/writing_hook_functions.rst | 39 ++++++++++++++++++++++++ doc/en/reference/reference.rst | 12 ++++++++ src/_pytest/config/__init__.py | 12 ++++++-- src/_pytest/nodes.py | 10 ++++-- src/_pytest/stash.py | 29 ++++-------------- src/pytest/__init__.py | 4 +++ 7 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 changelog/8920.feature.rst diff --git a/changelog/8920.feature.rst b/changelog/8920.feature.rst new file mode 100644 index 000000000..05bdab6da --- /dev/null +++ b/changelog/8920.feature.rst @@ -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. diff --git a/doc/en/how-to/writing_hook_functions.rst b/doc/en/how-to/writing_hook_functions.rst index de2ff3bdc..33b9c86cf 100644 --- a/doc/en/how-to/writing_hook_functions.rst +++ b/doc/en/how-to/writing_hook_functions.rst @@ -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 `. + +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. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 85a565b9b..b9b68c01d 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -962,6 +962,18 @@ Result used within :ref:`hook wrappers `. .. 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 ---------------- diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index feca31f4c..18423a47e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -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( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 939c7b207..e695f89bb 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -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: diff --git a/src/_pytest/stash.py b/src/_pytest/stash.py index 30786d6ef..e61d75b95 100644 --- a/src/_pytest/stash.py +++ b/src/_pytest/stash.py @@ -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",) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 3694f0fc4..bfdc7ae1b 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -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",