from Pixabayfrom Pixabay

上個月,我將工作上幾個專案的 Python linter、formatter,從原本的 Flake8、isort、Black Formatter 遷移至 Ruff

本來以為很簡單,應該半小時就可以搞定。不料細節比想像中的多,前前後後還是花了近 2 小時。

可能我比較龜毛吧!

正因耗費的時間有點多,所以寫下這篇懶人包教學,作為讀者遷移時的參考。

本文主旨與目標讀者

這篇文章是寫給,目前正使用上述 Flake8、isort、Black Formatter 作為格式化工具,並打算遷移至 Ruff 的 Python 開發者。

受眾就是〈VS Code 設定 Python Linter、Formatter 教學〉中描述的那樣。在專案中使用上述工具,也安裝了相關的 VS Code 套件,設定好 pre-commit hook,以及使用 pyproject.toml 作為這些工具的設定檔。

而本文的目標,就是讓專案的 linter、formatter,最終都統一使用 Ruff。

系列:Python Ruff 教學


為什麼要用 Ruff?

在系列第一篇,即〈Python 開發:Ruff Linter、Formatter 介紹 + 設定教學〉中,我闡述了使用 Ruff 的兩大理由

在工作上用了 Ruff 近 2 個月後,我覺得這兩個理由都很真實。而且,速度帶來的差異與感受,比我想像的更多!

除此之外,還有一個我之前沒想到的優點——「避免衝突」。

工具間的行為衝突

我們知道,linter 與 formatter 如果進行客製化設定,那兩者對於格式的要求可能會發生不一致,這在設定上需要留意。

比如最常見的「單行最大字元上限」。如果在 Flake8 從 79 字元改成 100,那 isort 和 Black 也要跟著設定才行!

尤其 isort 和 Black 都是格式化器,兩者都有「格式化程式碼」的能力,如果設定不一致,會帶來一定的困擾。

所以 isort 才會有以下這個常見設定

1
2
[tool.isort]
profile = "black"

為的就是和 Black「達成一致」。

而這些潛在的「衝突」議題,在統一使用 Ruff 後,將不復存在。


遷移任務清單

我們先看一下,遷移至 Ruff 需要完成的任務有哪些:

  1. 更新 pyproject.toml:新增、移除套件。更新工具設定檔內容。
  2. 更新 VS Code 套件、設定格式化行為。
  3. 更新 pre-commit 設定。
  4. 統一格式化專案所有程式碼。

以下說明其中的重點。

一、更新 pyproject.toml

這是所有步驟中,比較需要費心的部分。

首先是移除舊套件,並新增 Ruff。

在此假設你也是用 Poetry 來管理虛擬環境,所以我們可以直接修改 pyproject.toml 的 Poetry 設定部分:

1
2
3
4
5
[tool.poetry.group.dev.dependencies]
ruff = "0.4.5" # 版本必須固定,與 pre-commit 一致
# 移除 flake8 = "6.1.0"
# 移除 black = "23.10.1"
# 移除 isort = "5.12.0"

修改完成後,記得執行poetry lockpoetry install

一般使用 Poetry,套件版本的指定我們會採^運算子,讓套件有一個自動升級範圍,增加相容性。

但 linter、formatter 這類工具,因為還要配合 pre-commit hook,所以我幾乎都是固定版本、手動更新——尤其 Ruff 更新版本就要重新建立 pre-commit hook 的虛擬環境,過程相對耗時。

Ruff Linter、Formatter 設定

接下來是重頭戲——在 pyproject.toml 中設定 Ruff 行為。我們先看結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[project]
requires-python = ">=3.11" #影響pyupgrade檢查與自動修正的版本

[tool.ruff] # https://docs.astral.sh/ruff/settings/#top-level
line-length = 100
exclude = ["**/migrations/", "**/manage.py"]

[tool.ruff.lint] # https://docs.astral.sh/ruff/settings/#lint
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
]
ignore = [
"E402", # module level import not at top of file
]

[tool.ruff.format] # https://docs.astral.sh/ruff/settings/#format
quote-style = "double" # 引號風格,雙引號是預設值,這裡只是明示這個設定

為了教學說明與維護方便,我加上了大量註解

強烈建議你在工作專案中,也要為這些設定適度加入註解——畢竟不是每個人都熟悉 Ruff。而且,Ruff 可設定的項目眾多,久了自己可能也會忘記。


大部分的設定,我們在第一篇都已提過,不再重複。

以下補充一些值得留意的點。

一、必須明確區分 Linter 和 Formatter 設定

從 Ruff 0.2 版開始,對於 section 名稱(即tool.rufftool.ruff.format等)有比較嚴格的要求。可以參考這個 PR

簡言之,共用設定放tool.ruff。如果是 linter 專用的設定就要放tool.ruff.lint,Ruff Formatter 的設定則要放tool.ruff.format

不像以前那麼寬鬆。

設定不合法時,會有錯誤提醒,留意一下即可。

二、requires-python 設定

這個 key 是讓你指定專案的 Python 版本,有兩種表達方式。第一篇我們用的是這種,本文用的是第二種。

官方文件推薦使用第二種:

If you’re already using a pyproject.toml file, we recommend project.requires-python instead, as it’s based on Python packaging standards, and will be respected by other tools.

我們從善如流。

三、quote-style

最後不忘提醒,Ruff formatter 的 quote-style 設定,將會使整個專案的「引號」風格趨於統一(PEP 8 慣例除外,比如 docstring)。

換句話說,有些人習慣用單引號,有些則是雙引號,遷移後將會統一。

這對任何專案都是不小的變動,遷移前需要認真考慮,尤其是團隊對於要用哪一種風格,是否已達成初步的一致

比如我自己是單引號支持者,但是在工作中,為了提高大家採用新工具的意願,我願意稍作妥協。所以上面的例子是雙引號。


二、更新 VS Code 套件

這部分就很簡單了。

移除舊套件,安裝新套件。但如果你還有其他專案需要使用 Flake8 等工具,那也不能輕易移除。此時可以參考前一篇的「在工作區停用」。

以免不同套件相互干擾。

至於 Python 部分的 VS Code 設定,可直接參考前篇。

三、更新 pre-commit 設定

這部分比以前簡潔很多:

1
2
3
4
5
6
7
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.5
hooks:
- id: ruff
args: [ --fix ] # 啟用ruff --fix自動修復,會有類似format的效果
- id: ruff-format

畢竟以前要設定三個工具,如今只剩下一個。

四、統一格式化

統一格式化,而不是一邊開發一邊格式化,以免造成功能更新格式化更新混淆,導致 code review 時的大量視覺干擾。

怎麼做呢?回到命令列以指令操作。

專案根目錄使用下列指令:

1
ruff format

就這麼簡單,0.1.7 版以後,它實際上有一個預設參數是「.」,也就是整個目錄。

詳情可參考官方文件

注意,有些檔案沒必要格式化,比如 Django 專案 migrations 目錄下的資料庫遷移檔,可以在 pyproject.toml 中設定exclude


補充:pyupgrade 真是棒!

Ruff 整合了 pyupgrade,讓我第一次認識到這個強大的工具。

我強烈建議,一定要開啟 pyupgrade 功能!讓專案中的 Python 語法維持更高程度的一致性。

畢竟,在團隊協作中,「一致性」有時比慣例更加重要。

例示:修正 type hints 語法

比如 Python type hints 中,關於 Optional 的寫法,3.9 以前會這樣寫:

1
2
3
4
from typing import Optional

def foo(arg: Optional[int]):
...

Optional 本身是 Union 在特定情況下的別名,但它常常讓人感到困惑。

而在 Python 3.10 之後,上述寫法可以改為:

1
2
def foo(arg: int | None):
...

後者顯然更加簡潔且直觀。(讓你一望即知,參數型別不是 int 就是 None)

不過話說回來,這個例子並不是一個好的 type hints 示範,理由可參考這篇〈Python Type Hints 教學:我犯過的 3 個菜鳥錯誤〉底下,良葛格的留言。

pyupgrade 功能與小結

具體來說,pyupgrade 會自動將舊的 type hints 語法轉換為新版語法。

上述的Optional,將自動修正為|寫法——只要我指定了 Python 3.10 以上的版本。

但 pyupgrade 會修正的部分,遠不止 type hints,這只是一個例子。

啟用 pyupgrade,可以自動幫你把這些舊語法轉換為新版(由 requires-python 指定目標 Python 版本)語法。

這不僅讓你的程式碼更乾淨,也能避免不同成員寫出風格不一致的 Python 程式碼。