from Pixabayfrom Pixabay

2024/05/10:重新編輯全文,新增、刪除部分內容,補充更多設定實例。調整大量段落表述方式,提升文章流暢度。可視為 v1.5 版。

Ruff 從去年(2022)6 月正式開源起算,已過去一年半,知道此工具的開發者也愈來愈多。雖然在去年就聽過它,但我卻一直沒有任何行動。

隨著前陣子 v0.1.0 的發布(先別覺得這版本號怎麼乍看像早期測試版本🤣,前一版可是 v0.0.292),我感覺時機已到,所以進行了一番研究、嘗試,並寫下了本文。

本文目錄

  1. 緣起
  2. 本文主旨與目標讀者
  3. Why Ruff?
  4. 本文範例程式碼
  5. Ruff 介紹篇
  6. Linter 部分:取代 Flake8 與 isort 等等
  7. Formatter 部分:取代 Black
  8. 單、雙引號議題
  9. Ruff 設定篇
  10. pyproject.toml 設定
  11. Ruff VS Code 套件設定
  12. pre-commit 設定
  13. Ruff 設定篇小結
  14. 工作上我還未採用 Ruff 的理由
  15. 結語:Time to Ruff

緣起

工作上,我們團隊使用的 linter、formatter 是常見的 Flake8、isort、yapf。這在以往的文章已有多次提及。

隨著 Ruff 的聲量與能見度日漸提高,作為一個 Code Formatting 愛好者,我自然也是躍躍欲試——最好是能夠直接應用到工作開發上!

因為團隊還不大(含我共 3 個後端開發者),所以改用 Ruff 是完全有可能的。

不過經過一番研究與考慮,我還是暫時推遲了工作上對 Ruff 的採用(只是把 formatter 從 yapf 換成 Black),原因會在文末說明

但是!我還是很推薦,從現在就開始使用 Ruff 作為你「個人開發」的預設 linter 甚至 formatter。而 Ruff 究竟有哪些吸引人之處?且容我娓娓道來。

本文主旨與目標讀者

我在〈Python 開發:pre-commit 設定 Git Hooks 教學〉曾提過:

考慮到「pre-commit」這個主題已經有數篇文章珠玉在前,我下筆前都已拜讀完。本文的論述重心會盡可能與這些文章錯開,或乾脆直接引用,以降低不必要的重複感。

而 Ruff 也是如此,推薦你先讀過這篇〈新世代的 Python Linter - Ruff〉,然後再繼續閱讀本文,以獲得更全面的理解

接下來對 Ruff 的介紹,將有不同的側重與著墨,以及更多設定細節。

本文主旨

本文主要分為三大部分

  1. 前言。
  2. Ruff 介紹篇。
  3. Ruff 設定篇。

一開始不免俗地先講述我研究與使用 Ruff 的動機與理由,接著介紹 Ruff 的主要功能——linter 與 formatter。包括它能取代的工具、特色、優勢。

而「設定篇」著重的,則是在初步了解 Ruff 之後,如果真要採用它,我們還需要處理好哪些環節,才能在開發中流暢地使用它。

目標讀者

本文的目標讀者有三:

  1. Python 開發中還沒有認真用過 linter、formatter 作為規範工具,想要開始導入,並希望用 Ruff 一次解決的人。
  2. 長期使用「Flake8 + isort + Black」這套 Python 開發經典組合,對 Ruff 有高度興趣,想要試試看的人。
  3. Code Formatting in Python 愛好者。

簡言之,這篇文章是寫給「已經用 Python 一段時間」的人,而非 Python 初學者。

不過話說回來,如果初學者一開始就肯使用 Ruff 好好規範自己所寫的 Python code,不正是一個絕佳的開端嗎?😎


Why Ruff?

什麼是 Ruff,一句話說就是:

An extremely fast Python linter and code formatter, written in Rust.

而我認為採用 Ruff 主要基於下面兩大理由

一、快,就是快

下圖來自 Ruff 的 GitHub 首頁,很好表達了為何要採用 Ruff——因為快。

用 Rust 寫的,快,自然不在話下。

而「極致的快」在很多時候會大幅改變你做事的方式——不過這對於大型專案可能才更有感。

對於中、小型專案,我覺得以下第二個理由更加重要。

二、All-in-One:整合眾多套件

不止是速度,Ruff 的「整合能力」也不容小覷。

按照官方文件,有了 Ruff 後,以下的工具都不必再安裝了:

想想是不是有一點「pyproject.toml」企圖統一天下,整合眾多工具設定檔的味道呢?

相關文章:Python 開發設定檔——pyproject.toml 的崛起(待發表)

上述這些工具,尤其是最常用的 Flake8、isort、Black,如果現在只需要用一個工具就能實現,而且還更快、更好。

那我們何樂而不為?


本文範例程式碼

本文中所提到的 Ruff 具體設定內容,包括 pyproject、VS Code、pre-commit 部分,都會加到這個 GitHub 專案中。

其中值得參考的檔案有:

  1. pyproject.toml
  2. .vscode/settings.json
  3. .pre-commit-config.yaml

前言結束,接下來我們進入正題。首先是對 Ruff 的介紹。

Ruff 介紹篇

Ruff 是一個 Python linter + formatter,雖然大部分人可能只使用它的 linter 部分,因為 formatter 還處於 beta 階段。

Ruff formatter 是一個相對獨立的功能,在開源後才開始建構。你完全可以只用 Ruff 的 linter,而依舊使用 Black 或 yapf 來格式化程式碼。

不過,即使只看 Ruff 的 linter 部分,它也並不是一個「單純的 linter」而已。因為它的實際行為超過了靜態分析(static analysis)。

所以,想要全面了解 Ruff,我們需要具體知曉,它能夠做到哪些事、取代哪些工具。

Linter 部分:取代 Flake8 與 isort 等等

Ruff 的核心部分就是它的 linter,這點無庸置疑。

而 Flake8 作為 Python 開發中最流行的 linter,自然是 Ruff 首要取代的目標。所以 Ruff 連錯誤代碼都盡可能與 Flake8 一致,也是考慮到遷移的成本

Autofix 功能

不僅如此,Ruff 還能取代一眾「帶有 formatter 功能的 linter」——isort 就是其中的代表。而常見的 pyupgrade 也是。

從這點可知,即使你沒有使用 Ruff 的 formatter 功能,它的 linter 部分還是帶有一定的 format 能力——也就是 autofix

這個特性真的很方便,但同時帶來了一定的複雜。

方便的是,你可以只安裝 Ruff linter,就獲得多種 linter 附帶的 format 行為。而複雜則在於:設定上的細節也比一般 linter 更多。

小結:Ruff linter 是個簡易版 formatter

我原本以為 Ruff(不考慮ruff-format部分)只是一個比較快的靜態分析工具,顯然事實並非如此。

總之,我們只要記得:Ruff linter 有著簡易 format(autofix)能力——它是個簡易版的 formatter。

這和 Flake8 只做單純的靜態分析不同,Ruff linter 在檢查過程中,能夠直接對程式碼進行修改。當然,如果你不喜歡,這功能是可以關閉的。


Formatter 部分:取代 Black

我相信從 Ruff 的開源之初,想過要做 formatter 功能。畢竟 Rust 這麼快,只做 linter 未免太可惜了!而且兩者在使用上往往密不可分。

和 linter 不同,formatter 具有強烈的排它性。不同的 formatter 之間,沒有相容可言。不像多個 linter 還可以疊加使用——如果你不嫌一堆警告很煩XD。

如果要寫一個全新的 formatter,就必須有足夠的理由,讓開發者願意放棄當前方案,採用你的新工具——這很不容易。

比較可行的做法,是相容並取代市場上現有的 formatter。

既然要選一個,那當然是選 Black——目前最流行的 Python formatter。

Black Formatter 相關文章:

和 Black 的相容性

因此,和對待 Flake8 一樣,Ruff formatter 必須與 Black 格式化後的結果,有高度的相容(一致)。否則你換了它的 formatter,卻帶來一堆格式化結果改動,絕對會造成遷移負擔與採用意願的下降。

按照官方文件中「Black compatibility 」這段可知:

Specifically, the formatter is intended to emit near-identical output when run over Black-formatted code. When run over extensive Black-formatted projects like Django and Zulip, > 99.9% of lines are formatted identically. When migrating an existing project from Black to Ruff, you should expect to see a few differences on the margins, but the vast majority of your code should be unchanged.

兩者的相容性(一致性)高達 99.9% 以上。意味著你可以放心地從 Black 換到 Ruff,而不用擔心格式化結果的差異。

(但是有一個「但書」,請見下面的「單、雙引號議題」)

友善提醒:formatter 遷移的變動,最好獨立一個分支或 commit,一次就把所有格式化差異都處理掉。而不要和其他功能性的變動混在一起。


單、雙引號議題

Python 允許開發者自由選擇要在程式碼中使用單引號或雙引號。

只有在比如 docstring 這種連續使用 3 個引號的場景,慣例上要求使用 3 個雙引號

如果你對 Black 有一些了解,應該會知道,早期 Black 是完全不管你習慣用單引號還是雙引號,它一律把你的 Python 程式碼格式化為雙引號!

這小小的硬性規則帶來了巨大的反彈,畢竟 Python 開發者中想必有不少人和我一樣,是「單引號」的支持者。

最後,Black 開發團隊也不得不妥協(這是 Black 少數的妥協,因為該工具本身就是以「不妥協」為賣點🤣),加入了skip-string-normalization選項。

Ruff Formatter 的不同處理方式

而 Ruff linter 作為 Black 的替代方案,也會遇到相同的「困境」。不同於 Black,目前 Ruff linter 提供的是quote-style這個選項:

1
quote-style = "single"  # 選項有 "single" 和 "double"

如果設為"single",Ruff 會把專案中的所有雙引號,都替換為單引號。不過,在上述慣例部分,Ruff 還是保留雙引號,不用擔心。

需要提醒的是,如果你的專案目前是單雙引號混用,因為 Ruff 會強制統一引號風格,這勢必會帶來不少的格式化差異。如上所述,這些變動最好統一獨立處理


接下來,我們要進入 Ruff 的「設定篇」。

Ruff 設定篇

當要採用一個新的 linter、formatter 時(尤其站在團隊開發考量),以下這三個部分的支援成熟度,是我一定會慎重考慮的。

設定檔(pyproject.toml 支援)

複雜的工具都會有自己的設定檔,讓你可以客製化需求。如果能支援 pyproject.toml 則會更受到我的青睞。

相關文章:Python 開發:pyproject.toml 介紹 + 使用教學

VS Code 套件支援

它能讓你在寫程式的同時,就能夠看到 linter 發出的警告,而不必等到 commit 之際才被 pre-commit 擋下來。

相較於 linter,formatter 更需要有自己的 VS Code 套件。讓你能直接在 VS Code 中進行格式化,而不用透過 CLI 指令或等到 pre-commit 時才自動修正。

即使我們只用 Ruff 的 linter 功能,但因為有「autofix」存在,如前所述,它本質上也是一個簡易的格式化器,所以最好有 VS Code 整合。

pre-commit 支援

pre-commit 是團隊協作中一道重要的關卡,我在「為什麼要使用 pre-commit?」中已有相當的闡述。

對於一個開發工具,我們主要關心的是:「它是否提供 pre-commit hook?」


以上 3 點,Ruff 都有完整支援。下面就來一一解說。

pyproject.toml 設定

Ruff 總共支援三種設定檔:pyproject.tomlruff.toml.ruff.toml

如果不知道怎麼開始,參考文件是最快上手的方式。以下的範例與解說,我採用設定檔格式是pyproject.toml

Ruff 因為功能眾多,設定上較為複雜。所幸,大部分時候,我們只需要設定一些基本的項目

基本設定與解說

首先,因為在pyproject.toml中,所以設定的 key 定是[tool.xxx]格式。

Linter 部分,一些基本的設定比如line-length和 Flake8 類似。其中select和沒有列出的ignore相對重要。

Ruff 預設只會顯示 EF 系列的錯誤訊息(而 Flake8 還有 W 系列)。想要增加或排除特定部分的錯誤訊息警示,就得透過select欄位調整。比如我的設定:

1
2
3
4
5
6
7
8
9
[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"] # I 和 UP 是 isort 和 pyupgrade

[tool.ruff.format]
quote-style = "single"

select加上了IUP。分別代表了 isort 和 pyupgrade。一旦你開啟了它們,Ruff 就會提示相關錯誤,並在有錯誤時自動修正。(autofix 預設為開啟

因為開啟了UP,所以我必須設定target-version(這裡為py310),意味著 Ruff 會將程式碼中舊的寫法自動轉換(autofix)為 Python 3.10 的寫法。

總之,可設定的項目非常豐富。

設定懶人包

如果你剛開始用 Ruff,不知道該設定哪些項目。我建議你可以先設定以下幾個:

1
2
3
4
5
6
7
8
[tool.ruff]
line-length = 100 # 依照自己的專案需求設定

[tool.ruff.lint]
select = ["E", "F", "I"] # 至少會加入 I,因為 isort 是必要的

[tool.ruff.format]
quote-style = "single" # 依照自己的喜好或專案需求設定

Ruff VS Code 套件設定

Ruff 的 VS Code 套件在 2022 年 12 月首次發表。

套件的重要性前面有提過,在此再次闡述:

  • Linter:在 IDE 中直接提示,不必等到後續流程才發現錯誤。
  • Formatter:在 IDE 直接格式化(尤其配合「存檔時格式化」),不需透過指令。

基於上述兩個需求,如果一個全新的 linter 或 formatter 沒有相關的 VS Code 套件,我不會考慮使用。

安裝與設定 Ruff 套件

套件安裝後就可以直接使用,如果你的專案中已有專屬的 Ruff 設定檔,我覺得不需要再特別設定 VS Code Ruff 套件部分。

不過還是提供我的設定:(以下為 VS Code settings.json內容)

1
2
3
4
"ruff.enable": true,
"ruff.organizeImports": true,
"ruff.fixAll": true,
"ruff.showNotifications": "onWarning",

特別提醒,如果你在專案中有已經有自己的 Ruff 設定檔,那就不要設定 VS Code Ruff 套件的「args」部分。比如:

1
2
"ruff.lint.args": ["--fix"],
"ruff.format.args": ["--fix"],

因為 VS Code 會優先使用settings.json的設定。

建議設定 VS Code Python

因此,Ruff 套件我覺得不特別設定也沒關係,有專案中的 Ruff 設定檔就夠了。

相對的,VS Code 的 Python 部分則建議要設定。開發會更順手。

具體內容,Ruff 套件首頁有範例。我們直接看最完整的版本

1
2
3
4
5
6
7
8
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
},
"editor.defaultFormatter": "charliermarsh.ruff"
}

效果說明:

  1. editor.formatOnSave:存檔時自動格式化(對所有 formatter 都有效)。
  2. source.fixAll:存檔時自動 fix。(類似 pre-commit 時,hook 的自動修正)
  3. source.organizeImports:存檔時自動排序 imports。

我想上述這些 Python 設定才是 Ruff 在 VS Code 中流暢使用的重點。

不同專案間切換

不一定每個專案都用 Ruff。在不使用 Ruff 的專案中,Ruff linter 還一直提示你錯誤,這樣會很煩。

此時可以讓 Ruff 在這個「工作區」停用!做法如下圖。

Formatter 部分,因為每個專案只能選定一種格式化器,選好就沒有衝突問題。


pre-commit 設定

pre-commit 設定相對單純,更細部的行為,hook 會自動讀取設定檔中的內容。

1
2
3
4
5
6
7
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.4
hooks:
- id: ruff
args:
- --fix
- id: ruff-format

這裡只要注意版本和 hook 的 id 即可。


Ruff 設定篇小結

Ruff 的設定真的滿多樣且可以很複雜。但是,如果你只是想要快速上手,其實只需要設定一些基本的項目。

如果你不清楚究竟有哪些項目可以調整,又想了解更多。

除了研究官方文件Github 頁面的 README 外,去看看那些已經採用了 Ruff 的開源專案設定檔,也是很好的學習!

比如,我就習慣參考 FastAPI 的 Ruff 設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[tool.ruff]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
"UP", # pyupgrade
]

ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # too complex
"W191", # indentation contains tabs
]

[tool.ruff.isort]
known-third-party = ["fastapi", "pydantic", "starlette"]

[tool.ruff.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true

而且 FastAPI 真的很貼心,還加了註解!


工作上我還未採用 Ruff 的理由

2024/05/10:目前團隊已經全面採用 Ruff。

結語之前,講講我在一番研究後,仍未建議團隊在此刻就採用 Ruff 的理由。

放到最後不是為了賣關子,而是看到這裡,你應該比較能夠理解其中的顧慮。

主要有三個。

一、設定相對複雜

首先我覺得 Ruff 的設定太多樣了,畢竟它一口氣整合了這麼多的工具。

而且要同時考慮 pyproject.toml、VS Code、pre-commit 的整合,加上各種 linter 在設定上的開關,想想有點頭痛。

這會帶來一定的認知負擔,同事可能會覺得「搞這些真的有必要嗎?」——其實我也這麼想🤣

二、需求不足

我們目前的專案最多只能算中型,用「Flake8 + isort + Black Formatter」經典組合已能運作良好。改用 Ruff,可能看不出太大差別。

如果看不出差別,設定又要重新調整、學習,難免讓人卻步。

三、讓子彈再飛一會兒

說起來,前兩個理由並不算什麼重大阻礙,只是也沒有明顯的動力。

我打算等 Ruff formatter 的「beta」字樣拿掉後,直接「一換三」一次到位。

所以,不妨讓子彈再飛一會兒。


結語:Time to Ruff

Ruff 對我的重要性,雖然不如 Poetry 套件管理器。但這樣的工具依舊讓人興奮且充滿期待。

我並不認為目前的 Python 開發一定要用上 Ruff,但值得你嘗試一下。

如果你也是一個 Code Formatting 愛好者,那麼 Ruff 絕對是一個值得你花時間研究的工具。

更重要的是,由 Rust 所引發的 Python 生態革命,現在才剛剛開始。