一、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 并創建核心對象 🛠?
import pluggy# 1. 創建插件管理器(起個名字,比如 'yifei')
pm = pluggy.PluginManager('yifei')
# 2. 創建 hook 聲明標記(標記哪些是“可被插件擴展的鉤子”)
hookspec = pluggy.HookspecMarker('yifei')
# 3. 創建 hook 實現標記(標記插件里的“鉤子實現函數”)
hookimpl = pluggy.HookimplMarker('yifei')
這三步是基礎:
pm
?是 “大管家”,負責管理所有插件、鉤子的注冊和調用~hookspec
?是 “鉤子聲明貼紙”:貼在函數上,說明 “這個函數是給插件擴展用的鉤子”~hookimpl
?是 “插件實現貼紙”:貼在插件函數上,說明 “這個函數是用來擴展鉤子的”~
?
定義主邏輯(小狗?Dog
?類)🐕
class Dog:def __init__(self):# 把 Dog 類里的鉤子(@hookspec 標記的函數)注冊到插件管理器pm.add_hookspecs(Dog) def say(self):print('woof! woof!') # 小狗基礎叫聲# 調用 hook!小狗叫完后,觸發 say_after 鉤子,傳參數 arg1=1, arg2=2pm.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
),插件里的函數要和它參數匹配才能生效~
?
定義插件(給小狗加魔法的插件們)🧩
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
),這樣插件管理器才能識別~
插件里的邏輯,就是 “小狗叫完后,額外執行的代碼”(比如打印收到的參數)~
?
四、注冊插件 + 運行!🚀
# 注冊插件:把插件對象交給插件管理器“大管家”
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 長這樣:
@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
,就知道:“哦~ 這里又在喊插件來干活啦!” 🎉