一、pytest源码走读方法
依赖库认知篇 📦
这是理解 pytest 源码的 "前菜",先认识 3 个超重要的小伙伴:
iniconfig
📄:像个 "文件小管家",专门负责读取 ini 配置文件(比如 pytest 的配置),让 pytest 知道你想怎么跑测试~packaging
🎁:是个 "包信息解析大师"!能解析 Python 包的名字、版本、依赖这些细节,就像给包做 "身份认证",pytest 里处理包相关逻辑少不了它~pluggy
🔌: pytest 灵魂级 "插件小管家"!专门管理插件系统,让 pytest 能灵活扩展功能,后续重点要掌握它哟~
简单说:想读 pytest 源码,得先和这 3 个小伙伴混熟,它们是 "入门门票"🎫
pytest 源码结构探索篇 🏠
找到 pytest 源码目录里的 src
文件夹,就像找到 "宝藏基地" 啦~里面:
_pytest
📂:藏着最核心的源码逻辑,是 pytest "心脏" 所在~pytest
📂:给普通用户用的接口,像 "对外营业窗口",你平时import pytest
用的就是它封装好的功能~py.py
📄:像个 "小补丁",让 py 工具包能正常工作,默默当 "幕后英雄"~
在 _pytest
包里,还要 "筛一筛":排除那些 _
开头的(3 个包 + 44 个文件),剩下的才是咱重点看的核心源码,像在 "淘金子"✨
插件相关代码占比篇 🔌
在 40 多个文件里,有 29 个带着 def pytest_
这种插件相关的定义,说明 pytest 里超!级!多代码和插件有关(大概占 3/4)!
这也侧面说明:pluggy
为啥那么重要 ------ pytest 靠它实现丰富的插件生态,让 pytest 能 "七十二变",适配各种测试需求 🦸
掌握 pluggy 用法(插件灵魂!) 🔮
插件是啥?用可爱话讲:
- 插件是软件的 "魔法贴纸"✨:给软件贴一个,就能解锁新功能(比如 pytest 装个插件,能新增测试报告样式、自定义断言啥的)~
- 插件能 "自由装卸"🧩:想用就装,不想用就卸,软件本体功能不受影响,主打一个灵活!
对 pytest 来说:
- 本体(软件)提供基础测试功能(跑用例、断言这些)🏗️
- 插件(魔法贴纸)负责扩展:想生成漂亮报告?装报告插件!想改测试收集规则?装自定义插件!让 pytest 能 "变身" 满足各种场景~
总结一下 "走读 pytest 源码" 学习路径:
- 先搞定 Python 语言 + 标准库基础(打铁还需自身硬 🔨)
- 吃透
iniconfig
packaging
pluggy
这 3 个依赖库(和它们交朋友 🤝) - 顺着源码结构(
src
里的_pytest
等),重点挖插件相关逻辑(毕竟占比超高 🔌) - 深入掌握
pluggy
用法(它是 pytest 插件生态的 "发动机" 呀!)
这样一步步走, pytest 源码就从 "密密麻麻看不懂",变成 "拆盲盒一样有趣" 啦~ 后续再看代码,就像 "认亲戚":哦~这里用了 pluggy 注册插件、那里靠 packaging 解析版本... 慢慢就通啦 🚀
(简单说就是:先熟依赖 → 看源码结构 → 抓插件核心 → 掌握 pluggy → 打通 pytest 源码任督二脉!)
二、pluggy 的用法~
可以把整个流程想象成给小狗 Dog
加装 "说话后自动触发插件" 的魔法系统🔮
前期准备:导入 pluggy 并创建核心对象 🛠️
python
import pluggy
# 1. 创建插件管理器(起个名字,比如 'yifei')
pm = pluggy.PluginManager('yifei')
# 2. 创建 hook 声明标记(标记哪些是"可被插件扩展的钩子")
hookspec = pluggy.HookspecMarker('yifei')
# 3. 创建 hook 实现标记(标记插件里的"钩子实现函数")
hookimpl = pluggy.HookimplMarker('yifei')
这三步是基础:
pm
是 "大管家",负责管理所有插件、钩子的注册和调用~hookspec
是 "钩子声明贴纸":贴在函数上,说明 "这个函数是给插件扩展用的钩子"~hookimpl
是 "插件实现贴纸":贴在插件函数上,说明 "这个函数是用来扩展钩子的"~
定义主逻辑(小狗 Dog
类)🐕
python
class Dog:
def __init__(self):
# 把 Dog 类里的钩子(@hookspec 标记的函数)注册到插件管理器
pm.add_hookspecs(Dog)
def say(self):
print('woof! woof!') # 小狗基础叫声
# 调用 hook!小狗叫完后,触发 say_after 钩子,传参数 arg1=1, arg2=2
pm.hook.say_after(arg1=1, arg2=2)
# 用 @hookspec 标记:这是一个"钩子",插件可以扩展它!
@hookspec
def say_after(self, arg1, arg2):
"""say 方法调用后,自动调用本方法(插件会扩展这个逻辑)"""
# 这里可以写默认逻辑(也可以不写,纯靠插件扩展)
pass
重点理解:
say()
是小狗的 "基础动作"(叫一声),但叫完后会触发钩子say_after
,让插件有机会 "插话"~@hookspec
标记的say_after
是 "钩子函数":它定义了 "插件可以扩展的接口"(参数是arg1, arg2
),插件里的函数要和它参数匹配才能生效~
定义插件(给小狗加魔法的插件们)🧩
python
class Plugin_1:
# 用 @hookimpl 标记:这是插件对 say_after 钩子的实现!
@hookimpl
def say_after(self, arg1, arg2):
print(f'我是插件1,我收到了软件的参数 {arg1}, {arg2}')
class Plugin_2:
@hookimpl
def say_after(self, arg1, arg2):
print(f'我是插件2,我收到了软件的参数 {arg1}, {arg2}')
插件的作用:
每个插件里的 say_after
函数,必须和钩子函数(Dog
里的 say_after
)参数一致 (都有 arg1, arg2
),这样插件管理器才能识别~
插件里的逻辑,就是 "小狗叫完后,额外执行的代码"(比如打印收到的参数)~
四、注册插件 + 运行!🚀
python
# 注册插件:把插件对象交给插件管理器"大管家"
pm.register(Plugin_1())
pm.register(Plugin_2())
# 创建小狗实例,让它叫一声~
dog = Dog()
dog.say()
运行后,输出会是:
woof! woof!
我是插件1,我收到了软件的参数 1, 2
我是插件2,我收到了软件的参数 1, 2
因为:
- 小狗
dog.say()
先执行基础叫声 →woof! woof!
- 触发
pm.hook.say_after(arg1=1, arg2=2)
→ 插件管理器找到所有注册的插件(Plugin_1
、Plugin_2
)里的say_after
函数,依次执行它们的逻辑~
整体流程总结🎬
- 准备阶段 :创建插件管理器
pm
、钩子标记hookspec
/hookimpl
→ 像准备魔法道具~ - 定义主逻辑 :小狗
Dog
有个动作say()
,动作后有个 "魔法钩子"say_after
→ 像给小狗装了个 "触发机关"~ - 写插件 :
Plugin_1
、Plugin_2
用@hookimpl
标记,实现say_after
逻辑 → 像给小狗准备 "魔法贴纸",贴了就能扩展功能~ - 注册 + 运行:把插件注册到管理器,让小狗叫 → 触发钩子,插件生效!小狗叫完,插件们 "插话" 打印内容~
这样,整个 pluggy 插件系统的流程就跑通啦~ 核心就是:主程序定义钩子(@hookspec
),插件实现钩子(@hookimpl
),插件管理器(pm
)负责协调调用~ 是不是像给程序装了 "可扩展的魔法接口",想加功能就写插件,超灵活!✨
三、pytest 里的 hook(钩子)机制
可以把 pytest 想象成一个 "爱交朋友的小机器人",靠 hook 到处 "勾搭插件",让自己功能超丰富~ 🤖
先理解核心概念:hook 是怎么让插件生效的?🔌
还记得之前的 pluggy 例子吗?pm
(插件管理器)调用 hook 时,所有注册过的插件里的同名 hook 函数,都会被一起调用!
- 主程序(比如 pytest)说:"我现在要触发
pytest_addhooks
这个 hook 啦~ 所有装了这个 hook 的插件,快来执行!" - 插件们听到召唤,就会 "同时响应",一起执行自己的逻辑~
这样,不用改主程序代码,只要写插件实现 hook,就能扩展主程序功能 → 这就是 "插件效果"!
pytest 的 hook 体系(pytest 如何用 hook 搞事情)🐍
pytest 内部重度依赖 pluggy 的 hook 机制,核心点:
1. pytest 的 hook 声明在哪里?
pytest 把所有的 hook 声明 放在 hookspec.py
文件里,大概有 52 个左右~ 每个 hook 长这样:
python
@hookspec(historic=True)
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
...
@hookspec
标记:说明这是一个 "给插件扩展用的钩子"~- 函数名
pytest_addhooks
是 hook 的 "身份 ID",插件里的函数要和它同名 + 参数匹配才能生效~
2. pytest 什么时候调用 hook?
pytest 对 hook 的调用,散落在各个源码文件里 ~ 比如处理配置、收集测试用例、运行测试、生成报告... 每个关键步骤,pytest 都会喊一句:"hook.pytest_xxx
快来执行!"(比如 pytest_cmdline_main
pytest_collectstart
等等)
举个栗子:
当你运行 pytest
命令时,pytest 内部会先处理配置(加载配置文件、解析命令行参数),然后调用 pytest_cmdline_main
这个 hook → 所有实现了这个 hook 的插件,都会被触发!
3. 第一个被调用的 hook 是啥?
pytest 启动后,第一个被调用的 hook 通常和 "初始化插件、加载配置" 有关(具体要看源码,但核心逻辑是:先启动插件管理器,再触发 hook 让插件参与初始化)~
不管第一个是谁,流程都是:
pytest 启动 → 初始化插件管理器 → 调用 hook → 插件们响应 → 一起完成任务
pytest 内部的 hook 玩法:疯狂调用,疯狂扩展!🤯
pytest 内部可以说 "满是 hook",核心逻辑就是:
- 处理配置 :加载配置文件、解析命令行参数(比如
pytest -v
里的-v
参数)~ - 调用 hook :在关键节点(比如
pytest_cmdline_main
)触发 hook,让插件有机会 "插手"~ - hook 套 hook:一个 hook 被调用时,可能又会触发其他 hook → 形成 "链式反应",让插件能在多个环节扩展功能~
用可爱的话讲:
pytest 就像一个 "爱热闹的派对主持人",每进行到一个环节(比如 "开始收集测试用例""生成测试报告"),就会大喊:"有没有插件想表演节目?!" → 插件们(实现了对应 hook 的)就会冲出来表演~ 而且一个环节的表演,还能触发下一个环节的表演邀请,派对越玩越嗨!🎉
为什么说 "代码无法感知自己是主程序还是插件"?🤔
回到最开始的 pluggy 例子:
- 主程序(比如
Dog
类)里的say_after
是 hook 声明~ - 插件(
Plugin_1
Plugin_2
)里的say_after
是 hook 实现~
但实际上,它们的代码结构几乎一样 (都是定义函数,用装饰器标记)~ 运行时,插件管理器(pm
)不管你是 "主程序的 hook" 还是 "插件的 hook",只要注册过、匹配上,就一起调用!
- 对代码来说,它不用关心自己属于 "主程序" 还是 "插件" → 只要写对 hook 函数,就能被调用~
- 对插件来说,只要实现了 hook,就像 "自动成为主程序的一部分",参与到流程里~
总结:pytest + hook + 插件 是怎么玩的?🎮
- pytest 是 "主程序框架" :定义了一堆 hook(在
hookspec.py
),每个 hook 对应一个 "扩展点"~ - 插件是 "功能扩展包" :实现 pytest 的 hook(同名函数 +
@hookimpl
标记),注册到 pytest 里~ - hook 是 "连接器":pytest 运行到关键步骤时,调用 hook → 插件们响应 → 一起完成功能~
简单说: pytest 靠 hook 搭建了一个 "可无限扩展的舞台",想加功能就写插件实现 hook,舞台(pytest)和演员(插件)互不干扰,但又能完美配合~ 这就是 pytest 插件生态如此强大的原因!✨
这样,从 pluggy 的基础用法,到 pytest 如何用 hook 玩出花,就串联起来啦~ 核心就是:hook 让主程序和插件解耦,想扩展功能只需要写插件实现 hook,超灵活! 下次看 pytest 源码,看到 hookspec.py
和各种 hook.pytest_xxx
,就知道:"哦~ 这里又在喊插件来干活啦!" 🎉