スポンサーリンク

[Python] Ruff を使って PEP8 準拠の空行をエラー検知、自動整形する方法

最近では 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 formatruff check は異なるもとであるということです。

公式のドキュメントによると、ruff format は black の代替となるコード整形のツールと目指しているそうです。

一方で、ruff check は Flake8, isort, pydocstyle, pyupgrade, autoflake など様々なツールの代替となる Linter を目指しているそうです。

ですので、ruff formatruff check で挙動が変わるのは当然のであると理解し、受け入れる必要があります。

将来的には Ruff は Fomatter と Linter の統合を見据えているそうですが、2025年4月時点ではまだ統合されておらず、独立した異なるコマンドとして実装されています。

ruff check で --preview が必要な理由

Ruff のドキュメントにも記載があったのですが、PEP8 に準拠した空行のルールはまだプレビューとのことなので、--preview オプションが必要になります。

公式ページのルールの一覧を見ると、空行に関するルールである、E301~ E306 は全てプレビューとなっています。

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

ですので、コマンド実行時に --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 は両方合わせて使うことを推奨しています。

ですので特別な理由がないのであれば、両方使うことで大体の問題は解決するのではないかと思いました。

参考になりますと幸いです。

Python
スポンサーリンク
ibukishをフォローする
スポンサーリンク
ibukish Lab+

コメント

タイトルとURLをコピーしました