最近では Python の Linter として Ruff を使っている人は多いと思います。また Ruff は Linter としてだけではなく、Formatter (コード整形)としての機能も兼ね備えており、かつて使っていた Flake8, isort, autopep8 などの様々なツールから Ruff に乗り換えた人も多いと思います。自分もその1人です。
今回は Ruff のコード整形を使っていて、PEP8 に準拠した空行にコード整形できなくて、色々と調べたので備忘録として記事にします。
Ruff で空行を自動整形する方法
前提として、使っているツールのバージョンや ruff の設定は下記の通りです。
[dependency-groups]
dev = ["ruff>=0.11.2"]
[tool.ruff.lint]
select = ["E"]
ignore = ["E501"]
[tool.ruff.format]
line-ending = "lf"
先に結論
今回自分が実行していたコマンドは下記でした。前提として、pycodestyle (E) をチェック対象に選択しています。
ruff check . --fix # 空行のコード整形はされなかった
このコマンドを実行した時に、PEP8 に準拠した空行が適用されませんでした。
以下のコマンドを実行すると、思っていた通りにコード整形されました。
ruff check . --fix --preview
または
ruff format .
違いとしては ruff check
を使った場合は --preview
オプションを追加しました。もしくは check
ではなくて format
を使用しました。
前提の話
大前提として、ruff format
と ruff check
は異なるもとであるということです。
公式のドキュメントによると、ruff format
は black の代替となるコード整形のツールと目指しているそうです。
一方で、ruff check
は Flake8, isort, pydocstyle, pyupgrade, autoflake など様々なツールの代替となる Linter を目指しているそうです。
ですので、ruff format
と ruff check
で挙動が変わるのは当然のであると理解し、受け入れる必要があります。
将来的には Ruff は Fomatter と Linter の統合を見据えているそうですが、2025年4月時点ではまだ統合されておらず、独立した異なるコマンドとして実装されています。
ruff check で --preview
が必要な理由
Ruff のドキュメントにも記載があったのですが、PEP8 に準拠した空行のルールはまだプレビューとのことなので、--preview
オプションが必要になります。
公式ページのルールの一覧を見ると、空行に関するルールである、E301~ E306 は全てプレビューとなっています。
ですので、コマンド実行時に --preview
をつけることで空行ルールの適用をしました。
ruff check . --fix --preview
ruff format では期待通りの動きをした理由
そもそも Linter とは異なるものなので動きも異なるものです。また Formatter は Black と完全互換があるわけではないですが、コード整形のアウトプットは 99% が Black と一致するそうです。
なので、ruff format
ではプレビューオプションなどをつけることなく、改行に関して期待通りのフォーマットを適用してくれました。
おまけ: 特定のルールのみプレビューを適用する
Ruff のコマンドを実行する時に、--preview
を付けることでまだプレビューとなっているルールを適用することができるのですが、ただオプションを付けるだけだと意図しなかったルールまでプレビューが適用されてしまいます。
特定のルールのみプレビューを適用するには下記のように設定します。
自分は pyproject.toml
に設定を書いていますが、ruff.toml
でもテーブル名の tool.ruff
が不要になるだけで同じように書くことができます。
[dependency-groups]
dev = ["ruff>=0.11.2"]
[tool.ruff.lint]
preview = true # プレビューを有効化
explicit-preview-rules = true # プレビュールールの厳密な選択を有効化
select = ["E", "E302"] # E302 のみプレビューを適用
ignore = ["E501"]
[tool.ruff.format]
quote-style = "single"
line-ending = "lf"
あとは、通常通りコマンドを実行するだけです。pyproject.toml
でプレビューを有効化しているので、--preview
のオプションは不要です。
ruff check . --fix
これで E302
適用することができます。
注意点としては explicit-preview-rules = true
を設定した場合、プレビュー版を適用したいコード (E302 など) を明記しないとそのルールは適用されません。
百聞は一見に如かず
実際に試す方がわかりやすいと思うので試してみます。
今回はこの Python コードを使っていきます。露骨にルール違反を散りばめています。
def hello() -> str:
return 'Hello!'
import datetime
def hoge():
return 'hoge'
def fuga(text: str):
if text == 'gen':
yield hoge()
else:
return hoge()
def over_indent():
pass
まずは、プレビューを適用しない下記の設定で実行します。
[tool.ruff.lint]
select = ["E"]
ignore = ["E501"]
実行すると下記のような結果になりました。
$ uv run ruff check .
Built sample @ file:///xxxxxxxxxxxxxxxxx
Uninstalled 1 package in 1ms
Installed 1 package in 2ms
sample.py:5:1: E402 Module level import not at top of file
|
5 | import datetime
| ^^^^^^^^^^^^^^^ E402
|
Found 1 error.
安定版として提供されている E402
(import文は一番上に書いてね) のルール違反を指摘されました。
次にプレビューを有効にします。
[tool.ruff.lint]
preview = true
select = ["E"]
ignore = ["E501"]
実行すると下記のような結果になりました。
$ uv run ruff check .
Built sample @ file:///xxxxxxxxxxxxxxxxx
Uninstalled 1 package in 2ms
Installed 1 package in 3ms
sample.py:5:1: E402 Module level import not at top of file
|
5 | import datetime
| ^^^^^^^^^^^^^^^ E402
|
sample.py.py:11:1: E302 [*] Expected 2 blank lines, found 1
|
9 | return 'hoge'
10 |
11 | def fuga(text: str):
| ^^^ E302
12 | if text == 'gen':
13 | yield hoge()
|
= help: Add missing blank line(s)
sample.py.py:19:1: E117 Over-indented
|
18 | def over_indent():
19 | pass
| ^^^^^^^^ E117
|
Found 3 errors.
先ほどの、E402
に追加して、E302
(関数と関数の間は空行2行ですよ) と、E117
(インデント多いよ) の2つのエラーの指摘をされました。
次はさらに explicit-preview-rules
を有効にします。
[tool.ruff.lint]
preview = true
explicit-preview-rules = true
select = ["E"]
ignore = ["E501"]
実行すると下記のような結果になりました。
$ uv run ruff check .
Built sample @ file:///xxxxxxxxxxxxxxxxx
Uninstalled 1 package in 1ms
Installed 1 package in 2ms
sample.py:5:1: E402 Module level import not at top of file
|
5 | import datetime
| ^^^^^^^^^^^^^^^ E402
|
Found 1 error.
プレビュー版の厳密な選択を有効にしが、ルールの選択を増やしてしないため、安定版の E402
のみ指摘されました。
では最後にプレビュー版を適用したいルールのコードを明確に指定してみます。
今回は E302
のみ指定しました。
[tool.ruff.lint]
preview = true
explicit-preview-rules = true
select = ["E"]
ignore = ["E501", "E302"]
実行すると下記のような結果になりました。
$ uv run ruff check .
Built sample @ file:///xxxxxxxxxxxxxxxxx
Uninstalled 1 package in 2ms
Installed 1 package in 2ms
sample.py:5:1: E402 Module level import not at top of file
|
5 | import datetime
| ^^^^^^^^^^^^^^^ E402
|
sample.py:11:1: E302 [*] Expected 2 blank lines, found 1
|
9 | return 'hoge'
10 |
11 | def fuga(text: str):
| ^^^ E302
12 | if text == 'gen':
13 | yield hoge()
|
= help: Add missing blank line(s)
Found 2 errors.
E402
に加えて、E302
も期待通り指摘してくれました。
さらにおまけ: ruff format をいきなり適用したくない
ruff format
を使えば空行に関してはプレビューの指定なくても適用できることがわかりました。
ただ、format
はいきなり修正が入るのが困るってケースもあるかもしれません。
そんな時は、--diff
もしくは、--check
オプションをつけるといいです。そうすると変更予定の箇所が表示されるだけで、実際の変更は行われません。
--check
をつけると変更予定のファイルが表示されます。
$ uv run ruff format --check
Would reformat: sample.py
1 file would be reformatted
一方で、--diff
をつけると変更予定の差分が表示されます。
$ uv run ruff format --diff
--- sample.py
+++ sample.py
@@ -8,6 +8,7 @@
def hoge():
return 'hoge'
+
def fuga(text: str):
if text == 'gen':
yield hoge()
@@ -16,4 +17,4 @@
def over_indent():
- pass
+ pass
1 file would be reformatted
まとめ
いかがでしたでしょうか?
Ruff の Github のディスカッションでも、ruff check と ruff format は両方合わせて使うことを推奨しています。
ですので特別な理由がないのであれば、両方使うことで大体の問題は解決するのではないかと思いました。
参考になりますと幸いです。
コメント