from Pixabayfrom Pixabay

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」企圖統一天下,整合眾多工具設定檔的味道呢?

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

那我們何樂而不為?


本文範例程式碼

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

它是我「Django Tutorial」系列文章的範例程式碼,恰好也適合作為其它教學文章的實際素材展示。

git clone本專案後,可以直接git checkout02-ruff分支。這個分支所在的 commit,就是本文的具體設定內容與改動。

你也可以直接在 GitHub 上查看 commit 內容


前言結束,接下來我們進入正題。首先是對 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 更多。

簡易版 formatter

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

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

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


Formatter 部分:取代 Black

我相信從 Ruff 的開源之初,就已經想過要成為一個 All-in-One 工具。畢竟 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% 以上,基本可以放心遷移。

附帶一提,如果你是從 yapf 遷移到 Black,就能夠明顯感受到格式化風格差異所帶來的困擾——程式碼變動的地方太多了!

這種情況最好還是獨立一個分支或 commit,一次就把所有格式化差異都處理掉。而不要直接繼續開發,讓開發中的程式碼、檔案隨著開發進度被新 formatter 自動格式化,因為這會很影響 code review——格式化變動和開發變動混雜。

還處於 Beta 階段

The Ruff formatter is available as a production-ready Beta as of Ruff v0.1.2.

引言中的「production-ready Beta」超連結,指向 Ruff Formatter 的官方介紹文章與發展現況總結。

所謂的 beta 並不是還有很多 bug,更像是「設計與方向上」還沒有完全底定——比如下面要討論的「單、雙引號」議題。所以才會在前面加上「production-ready」。

如果你想要在個人的生產環境中使用,我相信是沒什麼問題。畢竟 FastAPI 專案已經直接使用了

不過如果是公司專案,我還是會選擇觀望,不急於一時。

單、雙引號議題

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

只有在比如 docstring 這種連續使用 3 個引號的場景——即""" <內容> """——時,慣例上要求使用雙引號。見 PEP 257

For consistency, always use """triple double quotes""" around docstrings.

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

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

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

Ruff Formatter 發展中

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

1
quote-style = "single"

即使你選了 single,在上述的慣例部分,Ruff 還是會格式化為雙引號,不用擔心。

至於要不要像 Black 加上skip-string-normalization,目前似乎還沒有定論。

整體而言,這個議題仍然在持續中,有興趣可以關注這個 GitHub Issue 討論串

這裡只是提醒你,採用 Ruff formatter 會有「單、雙引號」議題(而且和 Black 的處理方式不完全相同)。

至少對我而言,這非常重要。


接下來,我們要進入 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。反之如果有大量的客製,那獨立一個設定檔可能是比較好的選擇。

如果不知道怎麼開始,參考文件是最快上手的方式。文件中的範例內容同時有著註解式的解說。

不過大部分時候,我們只需要設定一些基本的項目。比如以下是我目前的設定,包含了 linter 與 formatter 部分:

1
2
3
4
5
6
7
[tool.ruff]
line-length = 100
select = ["E", "F", "I", "UP"]
target-version = "py310"

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

設定解說

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

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

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

1
2
3
4
5
6
[tool.ruff.lint]
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
select = ["E4", "E7", "E9", "F", "B"]

# 2. Avoid enforcing line-length violations (`E501`)
ignore = ["E501"]

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

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

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


Ruff VS Code 套件設定

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

說真的,如果一個全新的 linter 或 formatter 沒有相關的 VS Code 套件,我絕不會考慮使用。

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

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

絕大部分情況下,linter 提示的錯誤都會直接被 formatter 自動修正,感覺上沒有開啟 linter 提示似乎也無妨?

但是,兩者在少數時候會有不同的行為,所以我認為 linter 的提示仍是必要的。

當然,另一方面也因為我已習慣看 linter 提示👀——沒有會很不自在!

安裝與設定 Ruff 套件

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

不過還是提供我的設定:

1
2
3
4
5
6
"ruff.lint.args": [
"--line-length=100",
],
"ruff.organizeImports": true,
"ruff.fixAll": true,
"ruff.showNotifications": "onError",

建議設定 Python 行為

Ruff 套件我覺得不設定也沒關係,有設定檔就夠了。

除非你不想要為每個專案一一建立 Ruff 設定檔,那就還是得弄一下(會套用到每一個專案)。同時也要考慮不同專案間的設定衝突問題——使用者全局設定 vs 專案設定。

相對的,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,但工作上還沒有。或是相反的情況。

此時記得要把可能發生衝突的 VS Code 套件(主要是 linter 和 isort)在「工作區」範圍內停用!做法如下圖。不然 linter 部分很可能一起運作,產生意料之外的結果。

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 的理由

結語之前,講講我在一番研究後,仍未建議團隊在此刻就採用 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 生態革命,現在才剛剛開始。