【pytest高阶】源码的走读方法及插件hook

一、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 源码" 学习路径:

  1. 先搞定 Python 语言 + 标准库基础(打铁还需自身硬 🔨)
  2. 吃透 iniconfig packaging pluggy 这 3 个依赖库(和它们交朋友 🤝)
  3. 顺着源码结构(src 里的 _pytest 等),重点挖插件相关逻辑(毕竟占比超高 🔌)
  4. 深入掌握 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

因为:

  1. 小狗 dog.say() 先执行基础叫声 → woof! woof!
  2. 触发 pm.hook.say_after(arg1=1, arg2=2) → 插件管理器找到所有注册的插件(Plugin_1Plugin_2)里的 say_after 函数,依次执行它们的逻辑~

整体流程总结🎬

  1. 准备阶段 :创建插件管理器 pm、钩子标记 hookspec/hookimpl → 像准备魔法道具~
  2. 定义主逻辑 :小狗 Dog 有个动作 say(),动作后有个 "魔法钩子" say_after → 像给小狗装了个 "触发机关"~
  3. 写插件Plugin_1Plugin_2@hookimpl 标记,实现 say_after 逻辑 → 像给小狗准备 "魔法贴纸",贴了就能扩展功能~
  4. 注册 + 运行:把插件注册到管理器,让小狗叫 → 触发钩子,插件生效!小狗叫完,插件们 "插话" 打印内容~

这样,整个 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",核心逻辑就是:

  1. 处理配置 :加载配置文件、解析命令行参数(比如 pytest -v 里的 -v 参数)~
  2. 调用 hook :在关键节点(比如 pytest_cmdline_main)触发 hook,让插件有机会 "插手"~
  3. 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 + 插件 是怎么玩的?🎮

  1. pytest 是 "主程序框架" :定义了一堆 hook(在 hookspec.py),每个 hook 对应一个 "扩展点"~
  2. 插件是 "功能扩展包" :实现 pytest 的 hook(同名函数 +@hookimpl 标记),注册到 pytest 里~
  3. hook 是 "连接器":pytest 运行到关键步骤时,调用 hook → 插件们响应 → 一起完成功能~

简单说: pytest 靠 hook 搭建了一个 "可无限扩展的舞台",想加功能就写插件实现 hook,舞台(pytest)和演员(插件)互不干扰,但又能完美配合~ 这就是 pytest 插件生态如此强大的原因!✨

这样,从 pluggy 的基础用法,到 pytest 如何用 hook 玩出花,就串联起来啦~ 核心就是:hook 让主程序和插件解耦,想扩展功能只需要写插件实现 hook,超灵活! 下次看 pytest 源码,看到 hookspec.py 和各种 hook.pytest_xxx,就知道:"哦~ 这里又在喊插件来干活啦!" 🎉

相关推荐
conkl2 小时前
构建 P2P 网络与分布式下载系统:从底层原理到安装和功能实现
linux·运维·网络·分布式·网络协议·算法·p2p
求知若渴,虚心若愚。3 小时前
Error reading config file (/home/ansible.cfg): ‘ACTION_WARNINGS(default) = True
linux·前端·ansible
LinDaiuuj4 小时前
最新的前端技术和趋势(2025)
前端
一只小风华~4 小时前
JavaScript 函数
开发语言·前端·javascript·ecmascript·web
π大星星️5 小时前
Nginx 四层(stream)反向代理 + DNS 负载均衡
运维·nginx·负载均衡
beyoundout5 小时前
HAproxy
linux·运维·服务器
程序猿阿伟5 小时前
《不只是接口:GraphQL与RESTful的本质差异》
前端·restful·graphql
仰望星空的凡人6 小时前
【JS逆向基础】数据库之MongoDB
javascript·数据库·python·mongodb
F_D_Z6 小时前
【PyTorch】图像多分类项目部署
人工智能·pytorch·python·深度学习·分类
若梦plus7 小时前
Nuxt.js基础与进阶
前端·vue.js