使用哪些工具,可以提升 Python 項(xiàng)目質(zhì)量
前記
在編寫項(xiàng)目時(shí),都會(huì)對(duì)代碼質(zhì)量有一定的追求, 比如代碼藝術(shù)、設(shè)計(jì)模式、 重構(gòu)設(shè)計(jì)等等。但是一個(gè)良好 Python 項(xiàng)目除了程序員本身的代碼質(zhì)量能力之外, 還有系統(tǒng)設(shè)計(jì)和代碼質(zhì)量工具等等。不過(guò)由于每個(gè)系統(tǒng)的設(shè)計(jì)都會(huì)有一些不同, 系統(tǒng)設(shè)計(jì)需要程序員一定的經(jīng)驗(yàn), 需要跟著項(xiàng)目去一起成長(zhǎng)。
代碼質(zhì)量工具可以都抽離出來(lái),應(yīng)用到每個(gè)項(xiàng)目中, 本文則是我對(duì)這些代碼質(zhì)量工具的簡(jiǎn)要使用總結(jié)。
0.提交代碼規(guī)范
每個(gè)團(tuán)隊(duì)或個(gè)人, 都必須要有一套自己的分支管理和提交代碼規(guī)范, 對(duì)于分支管理, 一般會(huì)選用 git flow
, 如果不太會(huì)使用, 前期可以使用git flow 備忘清單(https://danielkummer.github.io/git-flow-cheatsheet/index.zh_CN.html), 并且對(duì)于 master
, develop
等分支設(shè)置一些權(quán)權(quán)限。另外提交的信息也要有對(duì)應(yīng)的規(guī)范, 比如本次提交屬于哪種類型, 本次提交的功能是什么等等, 但是這個(gè)提交規(guī)范往往都沒有一個(gè)標(biāo)準(zhǔn), 只要團(tuán)隊(duì)和個(gè)人用的順心, 能通過(guò)這些規(guī)范來(lái)減少開發(fā)矛盾, 復(fù)盤代碼等等即可。我常使用的是:
- git commit -m"<issue_id>:<file change>:<operating>:<info>"
其中每個(gè)字段代表的含義如下:
-
issue_id: 代表一個(gè)issue的id, 在準(zhǔn)備寫功能或者修復(fù)一個(gè)bug時(shí),都應(yīng)該先提一個(gè)issue, 這個(gè)issue要詳細(xì)的寫明要修改什么,達(dá)到什么目的,然后再針對(duì)這個(gè)issue提交代碼
- file change: 代表文件的變化, 如增加, 刪除, 修改;也有人使用
+,-,*
來(lái)分別代表增加, 刪除, 修改 -
operating: 代表本次代碼變化, 具體有如下幾種
-
feat:新功能
-
fix:修復(fù)bug
-
doc:文檔改變
-
style:代碼格式改變
-
refactor:某個(gè)已有功能重構(gòu)
-
perf:性能優(yōu)化
-
test:增加測(cè)試
-
build:改變了build工具 如 grunt換成了 npm
-
revert:撤銷上一次的commit
-
-
info: 簡(jiǎn)要的說(shuō)明本次提交信息
1.項(xiàng)目環(huán)境管理-Poetry
一個(gè)項(xiàng)目最重要的就是跑起來(lái), 而大家基本會(huì)同時(shí)在本地開發(fā)多個(gè)項(xiàng)目, 每個(gè)項(xiàng)目用到的環(huán)境都是不一樣的, 所以就需要用到虛擬環(huán)境隔離。在Python中提供了一個(gè)叫 venv
的虛擬環(huán)境管理包,他非常穩(wěn)定, 同時(shí)功能也不是很多, 一般只用在服務(wù)器上, 對(duì)于本地開發(fā)來(lái)說(shuō), 都會(huì)想要更多的功能, 更加方便的對(duì)虛擬環(huán)境, 依賴包進(jìn)行管理, Python包管理領(lǐng)域相關(guān)工具很多, 包括爭(zhēng)議很大的 Pipenv
, 我在經(jīng)過(guò)多種嘗試后, 覺得 Poetry
比較好用, 坑也比較少。
Poetry(https://python-poetry.org/)官網(wǎng)的簡(jiǎn)介就是讓Python包安裝和依賴管理變得容易, 我覺得 Poetry
是最好用的, 他不止對(duì)包管理有很多的支持, 還有其他的拓展功能, 如方便的打包和發(fā)布, 腳本簡(jiǎn)寫等等。
在第一次大多數(shù)的Python項(xiàng)目編寫中, 基本上都是按以下流程進(jìn)行:
-
1.安裝對(duì)應(yīng)的Python版本
- 2.通過(guò)
python -m venv <name>
的方式在項(xiàng)目創(chuàng)建venv的虛擬環(huán)境 - 3.在使用的過(guò)程中通過(guò)
python -m pip install <name>
的方式安裝依賴 - 4.在代碼編寫完畢后通過(guò)
python -m pip freeze > requirements.txt
生成依賴文件
而 Poetry
則十分簡(jiǎn)單, 以下是 poetry
的創(chuàng)建流程:
1.1.創(chuàng)建項(xiàng)目
通過(guò)命令 poetry new
就可以創(chuàng)建一個(gè)項(xiàng)目手腳架
- ➜ poetry new example
- ➜ tree
- .
- └── example
- ├── example
- │ └── __init__.py
- ├── pyproject.toml
- ├── README.rst
- └── tests
- ├── __init__.py
- └── test_example.py
- 3 directories, 5 files
可以看到 Poetry
創(chuàng)建了一個(gè) example
的項(xiàng)目, 生成了對(duì)應(yīng)的文件夾以及包括項(xiàng)目信息的 pyproject.toml
。如果在已有項(xiàng)目, 則通過(guò)命令 poetry init
來(lái)初始化:
- ➜ example poetry init
- This command will guide you through creating your pyproject.toml config.
- # 交互bash, 通過(guò)該交互bash填寫項(xiàng)目信息。
- Package name [example]: example
- Version [0.1.0]: 0.0.8
- Description []: example project
- Author [so1n <qaz6803609@163.com>, n to skip]: n
- License []:
- Compatible Python versions [^3.7]:
- Would you like to define your main dependencies interactively? (yes/no) [yes] no
- Would you like to define your development dependencies interactively? (yes/no) [yes] no
- Generated file
- # 填寫完項(xiàng)目信息后會(huì)生成以下內(nèi)容, 之后會(huì)在剛才的路徑創(chuàng)建pyproject.toml文件, 并寫入。
- [tool.poetry]
- name = "example"
- version = "0.0.8"
- description = "example project"
- authors = ["Your Name <you@example.com>"]
- [tool.poetry.dependencies]
- python = "^3.7"
- [tool.poetry.dev-dependencies]
- [build-system]
- requires = ["poetry-core>=1.0.0"]
- build-backend = "poetry.core.masonry.api"
- Do you confirm generation? (yes/no) [yes] yes
1.2.創(chuàng)建虛擬環(huán)境
Poetry
默認(rèn)使用系統(tǒng)默認(rèn)的Python環(huán)境, 不過(guò)可以通過(guò) poetry env use <python version>
來(lái)指定Python版本, 之后就創(chuàng)建了一個(gè)虛擬環(huán)境了。默認(rèn)的虛擬環(huán)境配置是存放在 /home/{user}/.cache/pypoetry
目錄的, 可以直接查看配置了解:
- ➜ poetry config --list
- # poetry使用的緩存目錄的路徑
- cache-dir = "/home/so1n/.cache/pypoetry"
- experimental.new-installer = true
- installer.parallel = true
- # 默認(rèn)值為true,如果執(zhí)行 poetry install/poetry add時(shí)沒有虛擬環(huán)境,就自動(dòng)創(chuàng)建一個(gè)虛擬環(huán)境,設(shè)置為false的話,當(dāng)虛擬環(huán)境不存在時(shí),會(huì)將包安裝到系統(tǒng)環(huán)境
- virtualenvs.create = true
- # 默認(rèn)值為false,設(shè)置為true的話,會(huì)在當(dāng)前項(xiàng)目目錄下創(chuàng)建虛擬環(huán)境
- virtualenvs.in-project = false
- # 虛擬環(huán)境的路徑,默認(rèn)路徑 {cache-dir}\virtualenvs
- virtualenvs.path = "{cache-dir}/virtualenvs" # /home/so1n/.cache/pypoetry/virtualenvs
而默認(rèn)的使用習(xí)慣(包括一些第三方包) 都是認(rèn)為venv是創(chuàng)建在項(xiàng)目路徑下的, 同時(shí)這也方便管理。 poetry
可以通過(guò)如下命令進(jìn)行更改后再創(chuàng)建虛擬環(huán)境,達(dá)到在項(xiàng)目路徑下創(chuàng)建虛擬環(huán)境的效果:
- # 更改配置
- ➜ poetry config virtualenvs.in-project true
在虛擬環(huán)境創(chuàng)建好后可以通過(guò)
- ➜ poetry run <commod>
來(lái)執(zhí)行想要運(yùn)行的命令或者調(diào)用Python包, 也可以通過(guò) poetry shell
啟動(dòng)一個(gè)被虛擬環(huán)境包裹的交互shell.
1.3.安裝依賴
虛擬環(huán)境創(chuàng)建好后, 就可以安裝依賴了, 可以直接使用 poetry
的 add
命令安裝依賴, 其中帶有 --dev
表示他是開發(fā)環(huán)境依賴包(開發(fā)環(huán)境依賴包和生成環(huán)境依賴包區(qū)分是很有益的):
- ➜ poetry add flask
- ➜ poetry add pytest --dev
安裝依賴后可以看到 pyproject.toml
文件發(fā)生變動(dòng):
- ...
- [tool.poetry.dependencies]
- python = "^3.7"
- Flask = "^1.1.2"
- [tool.poetry.dev-dependencies]
- pytest = "^6.2.4"
- ...
文件中多了剛剛安裝的 flask
依賴和 pytest
依賴, 且 pytest
依賴是屬于dev依賴。在后面還可以通過(guò) poetry
的命令生成對(duì)應(yīng)的生產(chǎn)環(huán)境依賴文件 requirement.txt
和測(cè)試環(huán)境依賴文件 requirements-dev.txt
:
- # 生產(chǎn)環(huán)境
- poetry export -o requirements.txt --without-hashes --with-credentials
- # 測(cè)試環(huán)境
- poetry export -o requirements-dev.txt --without-hashes --with-credentials --dev
這樣區(qū)分測(cè)試環(huán)境和生產(chǎn)環(huán)境的依賴可以盡量的減少測(cè)試需要的依賴包對(duì)生成環(huán)境造成影響。
除了增加依賴外, poetry
還支持很多種依賴操作方法, 具體如下:
- # 查看依賴
- poetry show
- # 以樹形結(jié)構(gòu)查看依賴
- poetry show -t
- # 更新所有鎖定版本的依賴
- poetry update
- # 更新指定的依賴
- poetry update flask
- # 刪除依賴
- poetry remove flask
1.4.其他
對(duì)于一般的自用Python項(xiàng)目來(lái)說(shuō), 上面的 poetry
操作已經(jīng)夠了, 如果需要發(fā)布自己的包到pypi, 或者安裝github最新的并未發(fā)布的包則可以使用他的其他拓展命令, 具體可以見文檔(https://python-poetry.org/docs/)。個(gè)人覺得 poetry
已經(jīng)非常優(yōu)秀了, 但是由于缺少一個(gè)穩(wěn)定的維護(hù)團(tuán)隊(duì), 所以難免有bug, 這時(shí)候可以采用降級(jí)的方法解決, 比如安裝依賴失敗, 則可以使用 poetry run pip install
安裝包, 再手動(dòng)補(bǔ)上 pyproject.yml
文件。
2.代碼質(zhì)量工具
在大型的項(xiàng)目中, 一般都不追求花哨的代碼, 而是追求穩(wěn)定, 容易理解, 復(fù)雜度低的代碼, 最完美的代碼應(yīng)該是入行的人一看就能理解, 又能完美的解決需求。但是人無(wú)完人, 很多時(shí)候在寫代碼可能會(huì)出現(xiàn)一些小問(wèn)題, 而這些小問(wèn)題靠人來(lái)檢查是費(fèi)時(shí)費(fèi)力的, 同時(shí)又很難排查出來(lái),這時(shí)就需要代碼檢查工具了。一般代碼檢查工具分為三類, 一類是檢查代碼風(fēng)格, 并把不標(biāo)準(zhǔn)的代碼風(fēng)格格式化為標(biāo)準(zhǔn)的代碼風(fēng)格;另一類則是代碼邏輯檢查,他會(huì)檢查代碼邏輯, 代碼復(fù)雜度, 引用的包是否有問(wèn)題等等, 最后一類是代碼安全檢查, 比如是否在代碼中引入密鑰, 或者像在 Python
代碼中寫 eval
函數(shù)等。
2.1.flake8
Flake8
是由Python官方發(fā)布的一款輔助檢測(cè)Python代碼是否規(guī)范的工具,相對(duì)于目前熱度比較高的 Pylint
來(lái)說(shuō), Flake8
檢查規(guī)則靈活,支持集成額外插件,擴(kuò)展性強(qiáng)。 Flake8
是對(duì)下面三個(gè)工具的封裝:
-
1.PyFlakes:靜態(tài)檢查Python代碼邏輯錯(cuò)誤的工具。
-
2.Pep8:靜態(tài)檢查PEP8編碼風(fēng)格的工具。
-
3.NedBatchelder’s McCabe :靜態(tài)分析Python代碼復(fù)雜度的工具。
Flake8
除了支持上面3種功能外, 還支持通過(guò)插件的方式引入其他功能, 比如使用 flake8-docstrings
強(qiáng)制要求編寫函數(shù) docstring
等。
在項(xiàng)目中可以通過(guò) poetry add flake8 --dev
引入flake8到dev依賴, 然后通過(guò)在根目錄增加 .flake8
文件:
- [flake8]
- # 適當(dāng)提高最大行長(zhǎng)度
- max-line-length = 120
- # 設(shè)置最大復(fù)雜度
- max-complexity = 24
- # 忽略這些錯(cuò)誤類型
- ignore = F401, W503, E203
- # 忽略以下文件
- exclude =
- .git,
- .venv,
- __pycache__,
- scripts,
- logs,
- upload,
- build,
- dist,
- docs,
- migrations,
指定 Flke8
該如何執(zhí)行, 最后調(diào)用命令 poetry run flake8.
即可。
2.2.mypy
毫無(wú)疑問(wèn), Python的語(yǔ)法讓人能簡(jiǎn)潔的寫出代碼, 但是他的動(dòng)態(tài)語(yǔ)言特性會(huì)使大型項(xiàng)目變得不牢固, 而 mypy
的出現(xiàn)恰好能解決這一問(wèn)題。 mypy
是一個(gè)靜態(tài)類型檢查工具,它可以幫助我們像靜態(tài)語(yǔ)言一樣在運(yùn)行代碼之前就捕獲到某些錯(cuò)誤, 但是我們?cè)趯慞ython代碼時(shí), 要像靜態(tài)語(yǔ)言一樣, 會(huì)參數(shù)寫上他的類型, 這就是Type Hints, 通過(guò) mypy
和 Type Hints
的結(jié)合, 雖然會(huì)增加我們的代碼量, 但它可以引入如下好處:
- 1.可以使
IDE
通過(guò)類型推斷提供更好的代碼補(bǔ)全和提示功能, 方便項(xiàng)目重構(gòu)以及提前檢查出錯(cuò)誤。 -
2.強(qiáng)制你去思考動(dòng)態(tài)語(yǔ)言程序的類型可能會(huì)幫助你構(gòu)建更清晰的代碼架構(gòu)。
比如有如下一個(gè)函數(shù):
- def foo(a, b):
- return a + b
一般來(lái)說(shuō)無(wú)法知道這個(gè)函數(shù)要傳什么類型的參數(shù)進(jìn)去, 也許一開始是傳 int
變量, 后面變?yōu)?nbsp;str
變量, 而通過(guò) Type Hints
則可以指定這個(gè)變量的類型是什么, 以及返回的類型是什么, 經(jīng)過(guò)改造后將會(huì)變?yōu)椋?/p>
- def foo(a: int, b: int) -> int:
- return a + b
這個(gè)函數(shù)的a, b參數(shù)以及返回的值類型都被標(biāo)注為 int
類型, 這時(shí)候假如在程序內(nèi)有兩個(gè)調(diào)用:
- foo(1, 2)
- foo("a", "b")
他們雖然都能運(yùn)行, 但是可以通過(guò) mypy
檢查出第二種調(diào)用方式是錯(cuò)誤的。雖然這種示例簡(jiǎn)簡(jiǎn)單單, 看不出什么痛點(diǎn), 但是在復(fù)雜的邏輯中, 他的優(yōu)勢(shì)就非常明顯了。
在項(xiàng)目中可以通過(guò) poetry add mypy --dev
安裝依賴包, 然后通過(guò)在根目錄增加 mypy.ini
文件:
- # mypy的核心配置
- [mypy]
- # 指明函數(shù)的值類型也要檢查
- disallow_untyped_defs = True
- # 忽略一些import的錯(cuò)誤, 有些舊包架構(gòu)可能不符合mypy的要求
- ignore_missing_imports = True
- # 指明針對(duì)根目錄tests的配置
- [mypy-tests.*]
- # 指明忽略對(duì)這個(gè)范圍的檢查
- ignore_errors = True
指定 mypy
該如何執(zhí)行, 最后調(diào)用 poetry run mypy .
即可
2.3.自動(dòng)格式化代碼
Python是一個(gè)動(dòng)態(tài)語(yǔ)言, 而且不會(huì)對(duì)代碼風(fēng)格做強(qiáng)要求, 這就會(huì)導(dǎo)致一千個(gè)人一千種Python代碼風(fēng)格, 這同樣在大型項(xiàng)目中非常糟糕的...好在Python生態(tài)中有很多自動(dòng)格式化的工具, 但這里并不會(huì)詳細(xì)對(duì)比他們的差異, 只是簡(jiǎn)要介紹下我在試用了多種后保留了以下3個(gè)工具(適不適合自己團(tuán)隊(duì), 還是得自己試試才知道):
- Pycharm
- Pycharm
- poetry add autopep8 --dev
-
--in-place
: 直接對(duì)文件進(jìn)行更改, 而不是把差異打印出來(lái)(用它就要相信他)--exclude
: 排除哪些文件/文件夾不進(jìn)行格式化--recursive
: 遞歸的遍歷文件--remove-all-unused-imports
: 刪除所有未導(dǎo)入的依賴包--ignore-init-module-imports
: 刪除所有未導(dǎo)入的包時(shí)排除__init__.py
文件--remove-unused-variables
:刪除未使用的變量
- 2.isort, 這個(gè)工具主要是用來(lái)給import語(yǔ)句進(jìn)行格式化, 比如語(yǔ)句超出文件允許最大長(zhǎng)度自動(dòng)換行, 以及對(duì)import語(yǔ)句進(jìn)行自動(dòng)排序(這個(gè)功能對(duì)強(qiáng)迫癥來(lái)說(shuō)爽飛了)。isort可以通過(guò)
poetry add isort --dev
進(jìn)行安裝, isort支持pyproject.toml
文件配置, 以下是我的一個(gè)常用配置:
- [tool.isort]
- # 兼容black模式, 因?yàn)槭褂玫搅薭lack進(jìn)行自動(dòng)格式化
- profile = "black"
- # 當(dāng)import包過(guò)多超過(guò)文件長(zhǎng)度后需要換行時(shí), 采用哪種模式
- multi_line_output = 3
- include_trailing_comma = true
- force_grid_wrap = 0
- use_parentheses = true
- ensure_newline_before_comments = true
- # 每行的最長(zhǎng)長(zhǎng)度
- line_length = 120
- # 忽略的文件夾
- skip_glob = "tests"
- 3.black, 號(hào)稱不妥協(xié)的自動(dòng)格式化工具, 只要它認(rèn)為不合適的, 就自動(dòng)格式化, 沒有選擇的余地, 如果與團(tuán)隊(duì)標(biāo)準(zhǔn)不一樣的請(qǐng)慎用, 我是挺接受他的自動(dòng)格式化風(fēng)格的...。black可以通過(guò)
poetry add black --dev
進(jìn)行安裝, mypy同樣支持pyprojrct.toml
文件配置, 以下是我的一個(gè)常用配置(black的配置項(xiàng)不多):
- [tool.black]
- # 每行的最長(zhǎng)長(zhǎng)度
- line-length = 120
- # 當(dāng)前是哪個(gè)Python版本
- target-version = ['py37']
3.pre-commit
自動(dòng)格式化的工具引入到項(xiàng)目沒多久后就會(huì)開始尋求自動(dòng)化了, 因?yàn)槊看翁峤恢岸家謩?dòng)跑一些自動(dòng)格式化的腳本, 實(shí)在是太麻煩了, 好在有 pre-commit
這個(gè)專門為 git hooks
而生的工具。
pre-commit
是一個(gè)用于管理和維護(hù)多種語(yǔ)言的 git pre-commit hooks
框架,就像Python的包管理器 pip
一樣,可以通過(guò) pre-commit
將他人創(chuàng)建并分享的 pre-commit hooks
安裝到自己的項(xiàng)目倉(cāng)庫(kù)中。 pre-commit
的出現(xiàn)大大減少了我們使用 git hooks
的難度,只需要在配置文件中指定想要的 hooks
,它會(huì)替你安裝任意語(yǔ)言編寫的 hooks
并解決環(huán)境依賴問(wèn)題,然后在每次提交前執(zhí)行 hooks
。
3.1.安裝
一般來(lái)說(shuō), 通過(guò) pip install pre-commit
就可以安裝了, 但是為了環(huán)境隔離, 需要使用 poetry add pre-commit --dev
安裝, 安裝完后就可以在項(xiàng)目根目錄創(chuàng)建文件 .pre-commit-config.yaml
, 以下是我的配置, 除了上面提到的幾個(gè)工具外, 還有一些其他腳本的校驗(yàn)工具:
- repos:
- - repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.910
- hooks:
- - id: mypy
- - repo: https://github.com/PyCQA/isort
- rev: 5.9.3
- hooks:
- - id: isort
- - repo: https://github.com/psf/black
- rev: 21.7b0
- hooks:
- - id: black
- - repo: https://github.com/PyCQA/flake8
- rev: 3.9.2
- hooks:
- - id: flake8
- - repo: https://github.com/myint/autoflake
- rev: v1.4
- hooks:
- - id: autoflake
- args: ['--recursive', '--in-place', '--remove-all-unused-imports', '--remove-unused-variable']
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.2.0
- hooks:
- - id: check-ast
- - id: check-byte-order-marker
- - id: check-case-conflict
- - id: check-docstring-first
- - id: check-executables-have-shebangs
- - id: check-json
- - id: check-yaml
- - id: debug-statements
- - id: detect-private-key
- - id: end-of-file-fixer
- - id: trailing-whitespace
- - id: mixed-line-ending
文件中的內(nèi)容很簡(jiǎn)單, 它指明使用了哪些工具, 工具是哪個(gè)版本, 以及使用哪些 hook
(一個(gè)倉(cāng)庫(kù)可能有多個(gè)hook), 每個(gè)參數(shù)的解釋如下:
-
repo: 倉(cāng)庫(kù)url, pre-commit通過(guò)git來(lái)安裝存在于github的工具
-
rev: 每個(gè)工具的版本, 這里是利用到git的tag屬性
-
hooks/id: 每個(gè)倉(cāng)庫(kù)會(huì)有很多個(gè)hook, 通過(guò)hooks-id來(lái)選擇要使用的hooks
-
hook/id/args: 每個(gè)hook都支持一些參數(shù), args就是配置hook的參數(shù)
這些工具都會(huì)讀取根目錄的配置文件, 而 autoflake
我找不到他的 pyproject.toml
配置說(shuō)明, 所以直接通過(guò)的 args
參數(shù)配置參數(shù)。之后就可以直接調(diào)用 hook
腳本, 如果是第一次引入已有項(xiàng)目則應(yīng)該先手動(dòng)調(diào)用 poetry run pre-commit run --all-files
, 他會(huì)調(diào)用所有 hook
對(duì)項(xiàng)目進(jìn)行檢查, 然后再根據(jù)檢查結(jié)果對(duì)代碼和配置進(jìn)行調(diào)整。調(diào)整完畢之后可以調(diào)用 poetry run pre-commit install
把 hook
腳本進(jìn)行安裝,它會(huì)自動(dòng)安裝在 .git/hooks/pre-commit
。安裝后, 每執(zhí)行次 git commit
時(shí), 都會(huì)通過(guò) git hooks
機(jī)制自動(dòng)執(zhí)行腳本, 自動(dòng)對(duì)代碼進(jìn)行檢查和格式化。
上面的配置文件是我的常用配置, pre-commit
的hook有很多, 不止這些, 如有興趣可以到pre-commit hook合集(https://pre-commit.com/hooks.html)查閱所有hook
4.遠(yuǎn)程倉(cāng)庫(kù)自動(dòng)執(zhí)行
本地的hook只針對(duì)本地提交者, 而在團(tuán)隊(duì)協(xié)作中, 其他人員可以暫時(shí)屏蔽或者刪除hook文件, 導(dǎo)致本地hook沒辦法達(dá)到強(qiáng)制的作用, 所以團(tuán)隊(duì)一般會(huì)在Github&Gitlab中的 pre-recevice
階段配置一個(gè)自己的腳本, 用來(lái)跑上面的代碼檢測(cè)工具, 雖然兩種的做法有點(diǎn)不同, 但核心步驟都是一樣:
-
1.先拉取最新的代碼到容器里
- 2.安裝階段, 這時(shí)候會(huì)向容器安裝Python版本以及類似
Redis
容器等等 -
3.代碼檢查, 這時(shí)候會(huì)運(yùn)行代碼質(zhì)量檢測(cè)工具, 如果有一個(gè)檢測(cè)錯(cuò)誤, 那么就拒絕提交, 并顯示哪里錯(cuò)誤了, 如果沒有問(wèn)題就走下一步。
-
4.測(cè)試階段, 該階段會(huì)運(yùn)行測(cè)試用例,檢測(cè)測(cè)試代碼覆蓋率是否合格, 同樣的, 如果檢測(cè)不合格就會(huì)拒絕提交, 成功就進(jìn)入下一步。
- 4.風(fēng)格統(tǒng)一, 使用風(fēng)格統(tǒng)一插件, 如Python中的
isort
,black
等, 把項(xiàng)目的代碼進(jìn)行格式化。
一般每個(gè)公司都有自己的一套標(biāo)準(zhǔn) CI/CD
, 而他們的使用方法可能都會(huì)有些差別, 但核心原理也差不多, 以下會(huì)以開源項(xiàng)目為例介紹如何使用Github的action(這個(gè)功能是免費(fèi)的?。?!).
Gitlab的CI/CD相關(guān)文章比較多, 可以查閱網(wǎng)絡(luò)或查閱書籍《持續(xù)交付》(https://book.douban.com/subject/6862062/), 也可以查看文章:https://www.mindtheproduct.com/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/, 如果對(duì)Gitlab hook有興趣可以查閱Gitlab pre-receive webook 的添加與使用(treesir.pub/post/gitlab-pre-receive-webhook)
該例子來(lái)自于我的項(xiàng)目rap。首先在項(xiàng)目目錄創(chuàng)建 script
目錄, 這里面的目錄可以被本地調(diào)用, 但主要還是用于 Github action
, 首先創(chuàng)建一個(gè)install的腳本, 這個(gè)腳本用于安裝依賴包:
- #!/bin/sh -e
- # Use the Python executable provided from the `-p` option, or a default.
- [ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3"
- REQUIREMENTS="requirements-dev.txt"
- VENV="venv"
- set -x
- if [ -z "$GITHUB_ACTIONS" ]; then
- "$PYTHON" -m venv "$VENV"
- PIP="$VENV/bin/pip"
- else
- PIP="pip"
- fi
- "$PIP" install -r "$REQUIREMENTS"
注意這里是以 venv
為虛擬環(huán)境依賴的, 而不是我上面提到的 poetry
. 使用 venv
的原因是線上一般是一個(gè)機(jī)器跑一個(gè)項(xiàng)目, 同時(shí)生產(chǎn)的機(jī)器都追求穩(wěn)定, 這時(shí)候venv簡(jiǎn)單而穩(wěn)定的好處就體現(xiàn)出來(lái)了, 所以比較推薦在線上使用 venv
。上面這個(gè)腳本就是創(chuàng)建一個(gè)虛擬環(huán)境, 然后根據(jù) requirements-dev.txt
安裝測(cè)試環(huán)境依賴。
依賴部分搞定了, 接下來(lái)就是告訴 Github action
該如何進(jìn)行代碼質(zhì)量檢查了, 于是編寫一個(gè)check的腳本:
- #!/bin/sh -e
- export PREFIX=""
- if [ -d 'venv' ] ; then
- export PREFIX="venv/bin/"
- fi
- set -x
- echo 'use venv path:' ${PREFIX}
- ${PREFIX}mypy .
- ${PREFIX}flake8
- ${PREFIX}isort .
- ${PREFIX}black .
- ${PREFIX}autoflake --in-place --remove-unused-variables --recursive .
這個(gè)腳本就是簡(jiǎn)單的調(diào)用各個(gè)命令, 命令的順序就如同上面一樣, 先進(jìn)行代碼檢查, 再跑測(cè)試用例, 最后進(jìn)行代碼格式化。這里的命令沒有寫各個(gè)的配置, 因?yàn)樗麄兌紩?huì)自動(dòng)讀取項(xiàng)目下的配置文件, 與我們的本地hook保持一致。
給Github action調(diào)用的腳本創(chuàng)建好后, 就開始創(chuàng)建真正的Github action文件了。首先在項(xiàng)目創(chuàng)建 .github/workflows
目錄, 并在 .github/workflows
目錄創(chuàng)建 test-suite.yml
文件(文件的更多說(shuō)明見官方文檔(https://docs.github.com/cn/actions/learn-github-actions/introduction-to-github-actions)):
- ---
- # 指定workflows名稱
- name: Test Suite
- # 指定操作push到master, 或者提pr到master時(shí)才執(zhí)行
- on:
- push:
- branches: ["master"]
- pull_request:
- branches: ["master"]
- jobs:
- tests:
- # 設(shè)置任務(wù)名
- name: "Python ${{ matrix.python-version }}"
- # 選擇跑在哪種容器類型
- runs-on: "ubuntu-latest"
- # 設(shè)置變量, 這里設(shè)置多個(gè)Python版本表示會(huì)對(duì)每個(gè)Python版本都運(yùn)行一次
- strategy:
- matrix:
- python-version: ["3.6", "3.7", "3.8", "3.9", "3.10.0-beta.3"]
- steps:
- # 調(diào)用官方的檢查和安裝python版本
- - uses: "actions/checkout@v2"
- - uses: "actions/setup-python@v2"
- with:
- python-version: "${{ matrix.python-version }}"
- # 更改腳本權(quán)限
- - name: "Change permissions"
- run: |
- chmod +x scripts/install
- chmod +x scripts/check
- # 安裝依賴
- - name: "Install dependencies"
- run: "scripts/install"
- # 進(jìn)行檢查
- - name: "Run linting checks"
- run: "scripts/check"
文件編寫完畢后就可以推送代碼到遠(yuǎn)程了, 然后就可以到Github對(duì)應(yīng)的項(xiàng)目地址查看action執(zhí)行情況, 一般成功結(jié)果如下(這里只測(cè)一個(gè)Python3.7, 如果失敗了, 你還會(huì)收到郵件提醒):
也可以點(diǎn)開查看某個(gè)步驟的詳情, 比如檢查代碼的詳情:
5.總結(jié)
這些工具都是我慢慢實(shí)踐和整合后找到最符合自己的構(gòu)建 Python 項(xiàng)目質(zhì)量的工具集了, 但是這些工具只能檢查表面情況, 而其他情況如代碼邏輯是否有問(wèn)題, 則需要編寫測(cè)試用例后再運(yùn)行才能知道。
而有些團(tuán)隊(duì)甚至?xí)捎脡毫y(cè)試, 線上仿真測(cè)試等等, 這些工具/系統(tǒng)的引入和使用初期會(huì)帶來(lái)很大的學(xué)習(xí)和時(shí)間成本, 但它們卻能讓項(xiàng)目一直保持茁壯成長(zhǎng), 減少線上項(xiàng)目Bug出現(xiàn)的次數(shù)(當(dāng)然這些工具還有測(cè)試用例等等也要一起跟著維護(hù))。