背景信息
統一代碼風格首先需要定義參照的規范,每個團隊可能會有自己的規范,我們選擇的規范是 yapf + mypy + isort,如果保證所有的研發人員都遵循相關規范呢?
- 鼓勵 IDE 中對應的插件的安裝,通過直接對應的插件,在編寫代碼階段就能實時發現不符合規范的情況,修改成本最低;
- 通過 Pre-commit 在創建 commit 時執行檢查,并進行必要的自動格式化,提供統一的規范約束,成本次之;
- 在發起 Pull-Request 時拉取代碼執行檢查,并異步返回檢查結果,成本稍高一些,但是功能也更完備一些,不僅能可以進行靜態檢查,也可以進行必要的自動化測試;
而本次主要介紹的就是基于 Pre-commit 進行必要的代碼檢查與格式化,期間遇到一些問題,整理出來幫助后人少踩坑吧。
Pre-commit 簡單介紹
Pre commit 是 git 提供的預提交機制,可以在創建 commit 之前執行預定義的鉤子程序,從而方便執行必要的代碼檢查。
而在實踐中可能會需要執行大量的鉤子程序,如果來管理這些鉤子程序呢,Pre-commit 就是其中一個應用較多的框架,通過這個框架可以比較方便地管理大量的預提交鉤子程序,這樣簡化了維護成本。如何來使用 Pre-commit 框架呢?
- 安裝 Pre-commit 框架,一般情況下 pip 安裝下即可;
- 在工程中添加
.pre-commit-config.yaml
文件,需要安裝的鉤子程序都是維護在這個配置文件中的; - 通過
pre-commit install
安裝對應的鉤子程序;
后續在創建 commit 時就會依次執行安裝好的鉤子程序,如果不符合鉤子程序對應的規范,就會檢查失敗,commit 無法創建,類似如下所示:
Pre-commit 的使用主要關注的是配置文件 .pre-commit-config.yaml
的定義,配置文件的一個簡單例子如下所示:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooksrev: v2.3.0hooks:- id: check-yaml- id: end-of-file-fixer- id: trailing-whitespace- repo: https://github.com/psf/blackrev: 22.10.0hooks:- id: black
在配置文件中主要關注下面的字段:
repo
:指定鉤子對應的代碼庫rev
:指定代碼倉庫對應的版本hooks
:指定代碼庫中需要用到的鉤子
Pre-commit 支持的完整的所有的代碼倉庫與對應的鉤子見官方 Supported Hooks
具體實踐
項目中使用的是 yapf + isort + mypy 的組合,之前已經在工程中安裝完成,包管理是使用 poetry 實現的,因此格式化工具對應的配置都是定義在 poetry.yaml
文件中的。本次使用 Pre-commit 期望也能直接使用原有格式化工具的配置,從而保證與定義好的規范保持一致。
yapf
本次在 Pre-commit 中使用 yapf 時,配置文件 .pre-commit-config.yaml
中的定義如下所示:
- repo: https://github.com/google/yapfrev: 'v0.31.0'hooks:- id: yapf
在 Pre-commit 中使用 yapf 時,報錯 toml package is needed for using pyproject.toml as a configuration file
,但是直接調用 yapf 時可以正常執行的。
定位問題后發現,Pre-commit 安裝的鉤子是放在獨立的虛擬環境里面的,這個虛擬環境中沒有對應的 toml 包,因此執行 yapf 報錯
解決方案就是通過 additional_dependencies 指定對應的包依賴,這樣依賴的包才能被正確安裝,修改配置后即可正確執行,最終配置如下:
- repo: https://github.com/google/yapfrev: 'v0.31.0'hooks:- id: yapfadditional_dependencies: [toml]
isort
isort 主要用于進行代碼 import 順序的調整,定義的配置如下所示:
- repo: https://github.com/PyCQA/isortrev: 5.12.0hooks:- id: isort
mypy
項目中主要是使用 mypy 進行靜態的代碼檢查,初始定義的配置如下所示:
- repo: https://github.com/pre-commit/mirrors-mypyrev: 'v0.910'hooks:- id: mypy
增加測試代碼進行驗證后發現問題很多:
- Pre-commit 中使用的 mypy 雖然是增量提交的,但是很多沒有修改的文件中的問題也被提醒出來,導致需要修改的文件特別多;
- 原本在
pyproject.toml
中 mypy 配置中明確排除掉的文件中的問題也會上報出來,而手工執行 mypy 是被正常忽略的;
對于問題 1 定位后發現 mypy 增量提交時會遞歸對 import 導入的文件同時進行靜態類型檢查,從靜態類型工具的角度是可以理解的,因為需要確認調用方和定義的函數類型是否一致,因此需要遞歸導入和檢查,但是這樣就會導致增量提交失去意義,對于已有工程而言在發起新提交時會需要修改大量的文件。
問題 2 的原因其實也與這個 import 循環導入有關,mypy 配置時通過 exclude
參數排除掉文件,在 mypy 全量檢查時會跳過,但是如果是增量提交,通過 import 導入的文件依舊會執行靜態類型檢查,此時 exclude
就沒辦法排除掉了
對于提到的這兩個問題,github 上有不少人給 mypy 上報了異常,甚至原有 exclude
參數不能排除掉 import 文件的機制設計,有開發者提出了 force exclude 的 PR,但是截止目前而言,這個想法沒有被現有 mypy 的維護者認可
從 mypy 維護者的解釋來看,mypy 作為靜態類型檢查工具,是需要結合執行上下文來盡可能發現不符合靜態類型定義的問題,force exclude 會導致沒辦法根據調用上下文發現類型不匹配的問題,mypy 就失去了意義。從 mypy 作為靜態類型檢查工具的角度來看,這個解釋沒有太大問題,但是在 Pre-commit 中使用 mypy 確實就會出現上面所說的那些問題,導致根本不可用。因此 mypy 維護者提出了 建議解決方案,方案的解決思路如下:
- mypy 開啟對整個工程的代碼檢查,即傳遞參數
pass_filenames: false
,不要使用增量式傳遞新增文件的方式; - pre-commit 使用獨立的虛擬環境去安裝 mypy,會導致第三方庫的類型檢查失效,建議直接使用原有運行虛擬環境中的 mypy 進行檢查,通過
language: system
進行配置;
最終配置定義如下:
- repo: https://github.com/pre-commit/mirrors-mypyrev: 'v0.910'hooks:- id: mypyentry: mypy .language: systempass_filenames: false
測試確實能解決掉原先 exclude 不生效的問題,但是由于目前是全量檢查,因此需要先對工程中原有的不符合 mypy 規范的進行了修復后,再開啟對應的 mypy 檢查。雖然解決方案不夠完美,需要先進行一輪全局的修復,但是修復后工作良好。
總結
通過上面的配置調整,最終在工程中正常配置了 Pre-commit,保證了團隊代碼風格的一致性,Pre-commit 通過將必要的規范限制在開發環境,保證了對開發人員的統一風格約束,從而提升整體代碼質量,有興趣可以嘗試一下