# pytest 钩子执行时序与包装器机制详解
在 pytest 插件开发中,控制多个钩子函数的调用时序是核心能力之一。pytest 通过 `tryfirst`、`trylast` 标记以及 `hook wrapper`(包装器)机制,实现了灵活的钩子执行顺序编排。
## 基础执行逻辑:1:N 函数调用
对于任意给定的钩子规范(hook specification),pytest 会收集所有注册的实现函数,形成一个 N 个函数的列表。默认情况下,这些函数按注册顺序依次执行,最终返回一个包含所有非 None 结果的列表。
### firstresult 机制
部分钩子规范支持 `firstresult=True` 选项。当启用此选项时,pytest 会在遇到第一个返回非 None 值的函数后立即停止调用后续函数,并将该结果作为整体钩子调用的返回值。这常用于只需单一默认值或首个有效结果的场景。
## 包装器(Hook Wrapper)机制
包装器是一种特殊的钩子实现,它是一个生成器函数,且仅 yield 一次。pytest 在执行钩子时,会先调用所有包装器,并在到达 yield 点时才执行普通钩子实现。
### 包装器的执行流程
1. **前置处理**:所有标记为 `wrapper=True` 的包装器按顺序执行,直到第一个 yield 点。
2. **核心执行**:pytest 在 yield 点调用非包装器的普通钩子实现(包括带 `tryfirst`/`trylast` 标记的函数)。
3. **后置处理**:所有包装器继续执行 yield 之后的代码,直至返回结果或抛出异常。
### 异常与结果处理
- 若底层钩子抛出异常,包装器可在 try-finally 块中捕获并重新抛出、抑制或转换为其他异常。
- 包装器可修改 yield 后的结果(如 `return new_res`),也可直接返回 `(yield)` 以透传原结果。
- 对于可变对象,建议避免直接修改,而应创建新副本。
## 钩子执行顺序示例
考虑以下三个插件实现同一钩子 `pytest_collection_modifyitems`:
```python
# Plugin 1: tryfirst=True
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
# 最早执行(非包装器)
...
# Plugin 2: trylast=True
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
# 最晚执行(非包装器)
...
# Plugin 3: wrapper=True
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items):
try:
return (yield) # 在 yield 前可执行前置逻辑
finally:
# 在 yield 后执行后置逻辑,无论是否异常
...
```
**实际执行顺序如下:**
1. Plugin3(包装器)的 yield 前代码;
2. Plugin1(tryfirst);
3. Plugin2(trylast,即使无标记也会排在 tryfirst 之后);
4. Plugin3(包装器)的 yield 后代码。
若多个包装器同时存在,它们之间也可通过 `tryfirst`/`trylast` 调整相对顺序。
## 注意事项
- 钩子函数名称必须以 `pytest_` 开头,否则即使使用 `specname` 参数也会被忽略。
- `pytest_addhooks` 钩子不可与包装器混用。
- `conftest.py` 中的钩子实现仅在对应 conftest 注册时可用,且加载顺序晚于内置插件和第三方插件。
通过合理组合上述机制,开发者可精确控制钩子生命周期中的行为插入点,实现复杂的前置/后置逻辑与异常处理策略。