本文系《pytest源码剖析》系列内容
19. assertion
插件路径:_pytest.assertion
实现的 hook
调用的 hook
pytest_assertion_pass
pytest_assertrepr_compare
pytest_assertion_pass.get_hookimpls
定义的 fixture
无
插件功能
创建命令行参数--assert, 指定是否重写断言
创建 ini 配置enable_assertion_pass_hook,指定是否启用该钩子
创建便捷函数register_assert_rewrite,对指定模块进行断言重写
创建便捷函数install_importhook, 安装导入钩子
代码片段
略(太多了,而且很跳)
简评
在函数 install_importhook 中看到一个特殊的操作:
sys.meta_path.insert(0, hook)
头一次见陌生得很,chatGPT 之:
sys.meta_path 是一个包含导入钩子(import hooks)的列表。导入钩子是一组对象,它们定义了如何在导入时查找和加载模块。它们可以用于自定义模块导入的行为。导入时,系统会按顺序检查 sys.meta_path 中的每个导入钩子,以确定模块的位置和加载方式。
简单来说,可以自定义 import 的行为
...
pyconfig插件
在加载第三方插件之前,会尝试解析命令行参数----assert
根据参数,决定是否调用函数 install_importhook
如果调用成功,则利用函数返回值(导入钩子)对所有 pytest 第三方插件进行断言重写
...
函数 install_importhook 做了什么?
首先,根据 config 实例化AssertionRewritingHook
hook = rewrite.AssertionRewritingHook(config)
然后,将 hook 插入到sys.meta_path, 实现对 import 的自定义
sys.meta_path.insert(0, hook)
最后,并将其保存在 config.stash 和 pluginmanager.rewrite_hook
...
类 AssertionRewritingHook 做了什么,为什么能够进行断言重写?
这个类单独定义了一个文件,1k + 行,看得头昏脑涨
简单来说:
通过导入钩子的机制,拦截了 import 语句,
通过 ast 分析被导入模块的代码内容,
将代码中assert关键字重写为多个语句:
得到断言条件的结果:top_condition
得到断言条件的取反结果:negation
将变量名替换为具体的变量值
如果成功且enable_assertion_pass_hook为真,则调用改 hook
如果失败拼接各项内容,生成详细的断言提示
同时重写了其他关键字:
Name:用于表示标识符(变量名、函数名等)
BoolOp:用于表示布尔操作,如 and、or。
UnaryOp:用于表示一元操作符,如 not、-(负号)
BinOp:用于表示二元操作符,如 +、-、*、/ 等
Call:用于表示函数调用。
Starred:用于表示 * 运算符
Attribute:用于表示对象属性的访问
Compare:用于表示比较操作,如<或==
将修改结果保存为 pyc 文件,供 python 执行
...
插件中的几个 pytest 钩子,反而没那么复杂
pytest_collection:把 session 保存到导入钩子中
pytest_sessionfinish:把 session 从导入钩子中移除
pytest_runtest_protocol:
调用pytest_assertrepr_compare
判断 ini 配置 ,调用pytest_assertion_pass
pytest_assertrepr_compare:调用util.assertrepr_compare
...
总的来说,这个插件做了一件事情:
将 python 代码中assert用另外一种方式执行,称之为重写
这是改变 python 底层的复杂操作,所以它使用了一些复杂的技术:
sys.meta_path
ast
同时,为了断言是可配置式的,做了大量的工作进行判断、兼容
最后,为了断言细节是可自定义的,提供了 2 个 hook:
pytest_assertrepr_compare
pytest_assertion_pass
...
由于插件本身太过硬核,写的可能不是很清晰
如果有机会,我录一个视频,再详细的梳理一下
领取专属 10元无门槛券
私享最新 技术干货