代碼質(zhì)量保證的利器:Git 預(yù)提交鉤子
一、什么是預(yù)提交(Pre-Commit)?
Pre-commit 是一個(gè) Python 軟件包,能夠幫助我們更容易創(chuàng)建預(yù)提交鉤子(pre-commit hook)。鉤子是 git 原生的東西,是在執(zhí)行特定 git 命令前運(yùn)行的腳本。
可以在倉庫的 .git/hooks 目錄中找到鉤子,該目錄由 git 自動(dòng)創(chuàng)建。在這個(gè)目錄中,可以找到類似下面這樣的文件:
applypatch-msg.sample pre-commit.sample prepare-commit-msg.sample
commit-msg.sample pre-merge-commit.sample push-to-checkout.sample
fsmonitor-watchman.sample pre-push.sample update.sample
post-update.sample pre-rebase.sample
pre-applypatch.sample pre-receive.sample
.sample 擴(kuò)展名會(huì)阻止執(zhí)行這些鉤子。要啟用鉤子,請(qǐng)刪除 .sample 擴(kuò)展名并編輯文件。
不過這么做既繁瑣又對(duì)用戶不友好,而且很難通過版本控制進(jìn)行管理,這就有 pre-commit 的用武之地了,它為 commit 命令創(chuàng)建鉤子,可以自動(dòng)檢測代碼中的任何問題,并使腳本的創(chuàng)建工作天衣無縫。
它會(huì)創(chuàng)建一個(gè)在調(diào)用 git commit 命令時(shí)自動(dòng)執(zhí)行的配置文件。配置文件中的任何檢查失敗,都會(huì)終止提交,從而始終確保代碼庫的質(zhì)量和一致性。
二、數(shù)據(jù)科學(xué)為什么需要 pre-commit 鉤子?
作為數(shù)據(jù)科學(xué)家,為什么也需要學(xué)習(xí) git 鉤子?現(xiàn)在社區(qū)中出現(xiàn)了一種趨勢,認(rèn)為精通軟件工程越來越重要,而能夠?qū)⒆约旱哪P筒渴鸬缴a(chǎn)中是非常有價(jià)值的事情。
而利用 git 鉤子和 pre-commit 是確保穩(wěn)健部署機(jī)器學(xué)習(xí)模型,同時(shí)保證代碼質(zhì)量的一種技術(shù)。機(jī)器學(xué)習(xí)模型比較繁瑣,因此任何能讓工作流程自動(dòng)化并在模型投入生產(chǎn)前捕捉潛在錯(cuò)誤的方法都是有價(jià)值的。
三、使用方法
1. 安裝
安裝 pre-commit 非常簡單,和通過 pip 安裝其他 Python 庫一樣。我個(gè)人是用 Poetry,但都能正常工作。
pip install pre-commit # pip
poetry add pre-commit # I personally use poetry
運(yùn)行該程序即可確認(rèn)已安裝。
pre-commit --version
2. 配置文件
進(jìn)入代碼倉庫根目錄,在項(xiàng)目中創(chuàng)建 .pre-commit-config.yaml。運(yùn)行以下命令即可完成創(chuàng)建:
touch .pre-commit-config.yaml
該文件將定義在提交代碼前要運(yùn)行的內(nèi)容。示例如下:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: v6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings, flake8-import-order]
args: ['--max-line-length=88']
這里 repos 表示包含要運(yùn)行的鉤子的倉庫列表。第一個(gè)是包含鉤子的 pre-commit-hooks 倉庫:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
在這個(gè)例子里,id 是執(zhí)行的特定預(yù)提交鉤子的唯一標(biāo)識(shí)符。
然后,我們對(duì) black 和 flake8 代碼包采用相同的流程。
3. 執(zhí)行
定義配置文件后,需要執(zhí)行 pre-commit install,讓 pre-commit 知道你想使用指定的腳本在該倉庫上執(zhí)行預(yù)提交檢查。
然后,修改代碼并嘗試提交,應(yīng)該會(huì)看到預(yù)提交鉤子開始工作。
也可以執(zhí)行命令 pre-commit run --all-files 來查看 pre-commit 正在做什么。
如果想在不運(yùn)行鉤子的情況下提交代碼,可以執(zhí)行 git commit --no-verify 繞過鉤子。
好了,我們舉個(gè)例子!
四、示例
我在 Medium-Articles 倉庫中安裝了 pre-commit,用它來展示一個(gè)例子。
在倉庫中,有以下 Makefile 文件:
SHELL=/bin/bash
PHONY: install
install:
poetry install
PHONY: lint
lint:
poetry run black .
poetry run isort .
其中定義了兩個(gè)命令 install 和 lint 來設(shè)置環(huán)境,確保代碼沒有問題。
該倉庫的 .pre-commit-config.yaml 如下所示,與之前展示的模板略有不同。
repos:
- repo: local
hooks:
- id: lint
name: lint
entry: make lint
language: python
types: [python]
stages: [commit]
在本例中,鉤子所在的倉庫是本地的,即在我的項(xiàng)目中。該鉤子將執(zhí)行 entry 里定義的 make lint 命令,作為鉤子的一部分,該命令已在 makefile 文件中定義。
倉庫里有一個(gè)代碼文件是這樣的(relu.py):
# Import packages
import numpy as np
import os
import plotly.express as px
# relu function
def relu(x):
return np.maximum(0, x)
# Generate data
x = np.linspace(-5, 5, 100)
y = relu(x)
# Graph
fig = px.line(x=x, y=y)
fig.update_layout(template="simple_white", font=dict(size=18), title_text="ReLU", width=650, title_x=0.5, height=400,)
if not os.path.exists("../images"):
os.mkdir("../images")
fig.write_image("../images/relu.png")
fig.show()
我們嘗試提交這段代碼,看看會(huì)發(fā)生什么。
(medium-articles-py3.11) egorhowell@Egors-MBP Medium-Articles % git add .
(medium-articles-py3.11) egorhowell@Egors-MBP Medium-Articles % git commit -m "testing pre-commit"
[INFO] Initializing environment for local.
[INFO] Installing environment for local.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
lint.....................................................................Failed
- hook id: lint
- files were modified by this hook
poetry run black .
Skipping .ipynb files as Jupyter dependencies are not installed.
You can fix this by running ``pip install "black[jupyter]"``
reformatted /Users/egorhowell/Repos/Medium-Articles/Data Science Basics/relu.py
reformatted /Users/egorhowell/Repos/Medium-Articles/Time Series/Exponential Smoothing/holt_winters.py
All done! ? ?? ?
2 files reformatted, 67 files left unchanged.
poetry run isort .
Fixing /Users/egorhowell/Repos/Medium-Articles/Time Series/Exponential Smoothing/holt_winters.py
Skipped 2 files
make: Nothing to be done for `Data Science Basics/relu.py'.
鉤子失敗了,它重新格式化了 relu.py。現(xiàn)在這個(gè)文件是這樣的:
# Import packages
import os
import numpy as np
import plotly.express as px
# relu function
def relu(x):
return np.maximum(0, x)
# Generate data
x = np.linspace(-5, 5, 100)
y = relu(x)
# Graph
fig = px.line(x=x, y=y)
fig.update_layout(
template="simple_white",
font=dict(size=18),
title_text="ReLU",
width=650,
title_x=0.5,
height=400,
)
if not os.path.exists("../images"):
os.mkdir("../images")
fig.write_image("../images/relu.png")
fig.show()
可以看到,預(yù)提交鉤子成功運(yùn)行了!
五、總結(jié)和進(jìn)一步思考
Pre-commit 是一個(gè)實(shí)用的 Python 軟件包,能改善 git 鉤子的工作流程并簡化腳本。它旨在通過自動(dòng)化代碼檢查流程來保持軟件的高質(zhì)量并消除錯(cuò)誤風(fēng)險(xiǎn)。作為數(shù)據(jù)科學(xué)家,越來越多參與到模型部署中,可以通過 git 鉤子和 pre-commit 等技術(shù)來確保模型安全部署到生產(chǎn)環(huán)境。