英语 stash的大概意思是宝藏的意思,a secret store of valuables or money 。
Pytest中的 Stash类 是一个“类型安全的异构可变映射”,用于在像 Config / Node 这类对象上存放插件或模块的任意附加数据,而不会发生名字冲突或破坏封装。
- 核心思想
键由模块级的 StashKey[T]() 实例表示(每个 StashKey() 都是唯一的对象,按对象身份区分键)。
StashKey 是泛型:StashKey[T] 表示该 key 对应的值类型是 T,便于静态类型检查器推断和检查(类型安全)。
Stash 内部用普通字典存储:Dict[StashKey[Any], object],通过 StashKey 的对象身份避免字符串命名冲突。
这个类很像字典,但是和字典不太一样——最重要的区别就是 key,stash可以避免名字冲突。即同样的一个键值 "chen" , 可以通过某种技术共存于这个键值对中——它是如何做到的?
简单而言,Stash将键对象化了,Python中对象的id是惟一的。
看看它的实现——这里使用了 Python3.8旧式的泛型
class StashKey(Generic[T]):
__slots__ = ()
这个StashKey泛型类,内部没有什么内容,除了一个 __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)
注释部分基本上说清楚了Stash的实现要点和用法。
主要是内部的字典结构 Dict[StashKey[T], object]
当你要创建一个 StashKey的时候
这样
stash_key = StashKey[str]() # 注意使用括号是类对象化,获得唯一对象id,这也是Stash 处理名字冲突的技术
注意使用括号是类对象化,获得唯一对象id,这也是Stash 处理名字冲突的技术
比如我们连续使用相同的串 "test" 创建两个对象
obj_key1 = StashKey[str]()
obj_key2 = StashKey[str]()
这两个对象是不一样的 ,虽然在表面上我们看到的是两个名称相同的插件,或者其它什么东西
对象中可以 object[key] 这这样用的类,都实现了 __getitem__
赋值操作 obejct[key] = value 对应 __setitem__
关键字 del key 则对应协议 __delitem__
总结
Stash 其实是一个字典封装,它重新实现了 __getitem__ __setitem 等协议,使他用起来的感觉和字典一样,同时,它配合 StashKey 提供了最关键的功能 解决名称冲突—— 不依赖用户提供的字符串或者其它可哈希的值来建立字典。
- 下面有个例子展示了 Stash的用法
from typing import IO, Optional
import tempfile
from _pytest.stash import StashKey
from _pytest.config import Config
from _pytest.nodes import Item
# 在模块顶层定义 StashKey(每个 key 实例唯一,避免冲突)
config_logfile_key = StashKey[IO[bytes]]() # 存放会话级临时文件句柄
item_flag_key = StashKey[bool]() # 存放 item 级布尔标志
def pytest_configure(config: Config) -> None:
"""在配置阶段创建会话级资源并放到 config.stash"""
f = tempfile.TemporaryFile("w+b")
config.stash[config_logfile_key] = f
def pytest_unconfigure(config: Config) -> None:
"""在卸载阶段清理会话级资源并从 stash 删除"""
if config_logfile_key in config.stash:
f = config.stash[config_logfile_key]
try:
f.close()
finally:
del config.stash[config_logfile_key]
def pytest_runtest_setup(item: Item) -> None:
"""为每个测试项设置 item 级 stash 值(比如记录某个状态)"""
# 标记这个 item 已经通过 setup
item.stash[item_flag_key] = True
def pytest_runtest_teardown(item: Item) -> None:
"""测试项 teardown 时清理 item 级 stash"""
if item_flag_key in item.stash:
del item.stash[item_flag_key]
def pytest_runtest_call(item: Item) -> None:
"""运行阶段读取 item.stash 的示例(只读或做判断)"""
was_setup = item.stash.get(item_flag_key, False)
if not was_setup:
# 如果需要,可以根据 stash 的状态采取动作
item.warn_outcome = "setup_not_done"