これは、なにをしたくて書いたもの?
Pythonでソースコードを書くにあたって、そろそろリンターやフォーマッターを適用しておきたいなという気がしまして。
特にPython慣れしていないので、リンターに指摘して欲しいみたいなところがあります。
最近はRuffというものがよさそうなので、こちらを導入してみます。設定にはこだわらないことにします。
Ruff
RuffのWebサイトはこちらです。
GitHub - astral-sh/ruff: An extremely fast Python linter and code formatter, written in Rust.
RuffはRustで書かれたPython用のリンター、コードフォーマッターです。
An extremely fast Python linter and code formatter, written in Rust.
以下を特徴としているようです。
- 既存のリンター(Flake8など)やフォーマッター(Blackなど)より10〜100倍高速
- pipでインストール可能
pyproject.tomlのサポート- Python 3.13互換
- 変更していないファイルの再解析を避けるため、キャッシュをビルトイン
- 自動修正をサポート(不要なimportの自動削除など)
- 800以上のルールを実装していて、ポピュラーなFlake8プラグインやflake8-bugbearなどをネイティブに再実装
- Visual Studio Codeなどのファーストパーティーなエディターとのインテグレーションをサポート
- 階層的かつカスケードな構成をサポートしており、モノレポフレンドリー
既存の以下のツール群を置き換えることができるようなのですが、自分は他のツールの事情は知りません…。どういうジャンルにどういう
ツールがあるのか?という意味で押さえておきます。
- リンター
- Flake8: Your Tool For Style Guide Enforcement — flake8 7.1.0 documentation
- PyFlakes、pycodestyle、Ned Batchelder's McCabe scriptのラッパー
- pydocstyle’s documentation — pydocstyle 0.0.0.dev0 documentation
- docstringに関するもの
- Flake8: Your Tool For Style Guide Enforcement — flake8 7.1.0 documentation
- フォーマッター
- importの補正
- isort
- 並び替え
- autoflake · PyPI
- 使用していないimportの削除
- isort
またPython 3.13互換ということでどのバージョンのPythonをサポートしているのか気になるところでしたが、FAQを見ると
3.7以上のようですね。
Ruff can lint code for any Python version from 3.7 onwards, including Python 3.13.
FAQ / What versions of Python does Ruff support?
今回は設定は凝らず、ひとまず導入してみたり設定方法を少し調べるところまでにしておきます。
環境
今回の環境はこちら。
$ uv --version uv 0.5.14 $ python3 --version Python 3.12.3
Ruffをインストールする
まずはuvプロジェクトを作成。
$ uv init --vcs none hello-ruff $ cd hello-ruff
インストールはpip installで行う方法が紹介されています。
FAQではuvを使った場合も載っていて、uv add --devかuv tool installが紹介されています。
※FAQ自体はRustのインストールが必要か?ですが
FAQ / Do I need to install Rust to use Ruff?
今回はuv add --devを使うことにします。
$ uv add --dev ruff
pyproject.toml
[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.8.6",
]
インストールされたライブラリーの一覧。Ruff本体だけなんですね、良いです。
$ uv pip list Package Version ------- ------- ruff 0.8.6
まずはRuffのインストールができました。
Ruffを使ってみる
さて、どうしましょうか。適当なソースコードが必要なので、チュートリアルのものを使うことにします。
こういう構成にしました。
$ tree -P '*.py'
.
├── foo.py
└── numbers
├── __init__.py
└── bar.py
2 directories, 3 files
2つのPythonファイルがありますが、中身は同じです。
foo.py
from typing import Iterable import os def sum_even_numbers(numbers: Iterable[int]) -> int: """Given an iterable of integers, return the sum of all even numbers in the iterable.""" return sum( num for num in numbers if num % 2 == 0 )
numbers/bar.py
from typing import Iterable import os def sum_even_numbers(numbers: Iterable[int]) -> int: """Given an iterable of integers, return the sum of all even numbers in the iterable.""" return sum( num for num in numbers if num % 2 == 0 )
リンターとして使う
Ruffをリンターとして使ってみましょう。
使い方はチュートリアルにも書かれていますが、
こちらを見るのがよいでしょうね。
Ruffの設定はこうしました。[tool.ruff.lint]の部分がRuffのリンターの設定です。
pyproject.toml
[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.8.6",
]
[tool.ruff]
[tool.ruff.lint]
select = [
"E", # pycodestyle error
"W", # pycodestyle warning
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
]
[tool.ruff.format]
設定はpyproject.toml、ruff.toml、.ruff.tomlで行います。ruff.tomlまたは.ruff.tomlで書く場合は、pyproject.tomlにおける[tool]の部分が
なくなります。
記述方法はこちらを見るとよいでしょう。
RuffのリンターのデフォルトのルールはEとFのようですが、今回は上記の範囲で有効にしました。
Ruff would enable all rules with the E (pycodestyle) or F (Pyflakes) prefix, with the exception of F401.
The Ruff Linter / Rule selection
このEとかFとかはRuffのルールのプリフィックスです。
Ruff's linter mirrors Flake8's rule code system, in which each rule code consists of a one-to-three letter prefix, followed by three digits (e.g., F401). The prefix indicates that "source" of the rule (e.g., F for Pyflakes, E for pycodestyle, ANN for flake8-annotations).
The Ruff Linter / Rule selection
実行してみます。ruff checkでリンターを実行できます。
$ uv run ruff check foo.py:1:1: UP035 [*] Import from `collections.abc` instead: `Iterable` | 1 | from typing import Iterable | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035 2 | 3 | import os | = help: Import from `collections.abc` foo.py:1:1: I001 [*] Import block is un-sorted or un-formatted | 1 | / from typing import Iterable 2 | | 3 | | import os 4 | | 5 | | 6 | | def sum_even_numbers(numbers: Iterable[int]) -> int: | |_^ I001 7 | """Given an iterable of integers, return the sum of all even numbers in the iterable.""" 8 | return sum( | = help: Organize imports foo.py:3:8: F401 [*] `os` imported but unused | 1 | from typing import Iterable 2 | 3 | import os | ^^ F401 | = help: Remove unused import: `os` foo.py:7:89: E501 Line too long (92 > 88) | 6 | def sum_even_numbers(numbers: Iterable[int]) -> int: 7 | """Given an iterable of integers, return the sum of all even numbers in the iterable.""" | ^^^^ E501 8 | return sum( 9 | num for num in numbers | numbers/bar.py:1:1: UP035 [*] Import from `collections.abc` instead: `Iterable` | 1 | from typing import Iterable | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035 2 | 3 | import os | = help: Import from `collections.abc` numbers/bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted | 1 | / from typing import Iterable 2 | | 3 | | import os 4 | | 5 | | 6 | | def sum_even_numbers(numbers: Iterable[int]) -> int: | |_^ I001 7 | """Given an iterable of integers, return the sum of all even numbers in the iterable.""" 8 | return sum( | = help: Organize imports numbers/bar.py:3:8: F401 [*] `os` imported but unused | 1 | from typing import Iterable 2 | 3 | import os | ^^ F401 | = help: Remove unused import: `os` numbers/bar.py:7:89: E501 Line too long (92 > 88) | 6 | def sum_even_numbers(numbers: Iterable[int]) -> int: 7 | """Given an iterable of integers, return the sum of all even numbers in the iterable.""" | ^^^^ E501 8 | return sum( 9 | num for num in numbers | Found 8 errors. [*] 6 fixable with the `--fix` option.
カレントディレクトリー配下を、サブディレクトリー含めて見てくれるようですね。
自動で修正できるものについては、--fixオプションをつけることで修正できるようです。
$ uv run ruff check --fix foo.py:5:89: E501 Line too long (92 > 88) | 4 | def sum_even_numbers(numbers: Iterable[int]) -> int: 5 | """Given an iterable of integers, return the sum of all even numbers in the iterable.""" | ^^^^ E501 6 | return sum( 7 | num for num in numbers | numbers/bar.py:5:89: E501 Line too long (92 > 88) | 4 | def sum_even_numbers(numbers: Iterable[int]) -> int: 5 | """Given an iterable of integers, return the sum of all even numbers in the iterable.""" | ^^^^ E501 6 | return sum( 7 | num for num in numbers | Found 2 errors.
8個エラーになっていたものが、2個まで減りました。
残っているのはdocstringの長さオーバーですが、これは今回はそのままにしておきます。
修正後のファイルはこうなっていました。
foo.py
from collections.abc import Iterable def sum_even_numbers(numbers: Iterable[int]) -> int: """Given an iterable of integers, return the sum of all even numbers in the iterable.""" return sum( num for num in numbers if num % 2 == 0 )
リンターの設定はこちらおよびルールを見ながら設定することになります。
また、ここのリンターに対して以下のように[tool.ruff.lint.xxxxx]として設定できることを覚えておいた方がよさそうですね。
[tool.ruff.lint.flake8-bugbear] # Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`. extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
フォーマッターとして使う
最後にRuffをフォーマッターとして使ってみます。
やはり使い方はチュートリアルにも書かれていますが、
こちらを見るのがよいでしょうね。
実行はruff formatなのですがファイルが直接変わってしまうので、いきなり実行するのではなく--checkまたは--diffオプションで
チェックと差分を見てみましょう。
$ uv run ruff format --check $ uv run ruff format --diff
結果はそれぞれこうなりました。
$ uv run ruff format --check
Would reformat: foo.py
Would reformat: numbers/bar.py
2 files would be reformatted, 1 file already formatted
$ uv run ruff format --diff
--- foo.py
+++ foo.py
@@ -3,7 +3,4 @@
def sum_even_numbers(numbers: Iterable[int]) -> int:
"""Given an iterable of integers, return the sum of all even numbers in the iterable."""
- return sum(
- num for num in numbers
- if num % 2 == 0
- )
+ return sum(num for num in numbers if num % 2 == 0)
--- numbers/bar.py
+++ numbers/bar.py
@@ -3,7 +3,4 @@
def sum_even_numbers(numbers: Iterable[int]) -> int:
"""Given an iterable of integers, return the sum of all even numbers in the iterable."""
- return sum(
- num for num in numbers
- if num % 2 == 0
- )
+ return sum(num for num in numbers if num % 2 == 0)
2 files would be reformatted, 1 file already formatted
--checkはファイルをフォーマットせず、チェックだけ行います。これで、どのファイルがフォーマットされるのかを確認できます。
-diffもファイルをフォーマットせずチェックを行いますが、合わせて変更内容を表示してくれます。
フォーマットする時にはこれらのオプションを外します。
$ uv run ruff format
フォーマッターを実行した結果。
foo.py
from collections.abc import Iterable def sum_even_numbers(numbers: Iterable[int]) -> int: """Given an iterable of integers, return the sum of all even numbers in the iterable.""" return sum(num for num in numbers if num % 2 == 0)
フォーマッターに関する設定はこちら。
今回は特に設定したいものがなかったので、[tool.ruff.format]という枠だけにしました。
pyproject.toml
[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.8.6",
]
[tool.ruff]
[tool.ruff.lint]
select = [
"E", # pycodestyle error
"W", # pycodestyle warning
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
]
[tool.ruff.format]
フォーマッターに関しては、Settingsに記載されている内容以上の設定はできないようです。
Given the focus on Black compatibility (and unlike formatters like YAPF), Ruff does not currently expose any other configuration options.
The Ruff Formatter / Configuration
pyproject.tomlの設定
ベースにしていたpyproject.tomlの設定を載せておきます。使っていって気が変わったら、こちらの設定内容を変更していくかもしれません。
pyproject.toml
[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.8.6",
]
[tool.ruff]
[tool.ruff.lint]
select = [
"E", # pycodestyle error
"W", # pycodestyle warning
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
]
[tool.ruff.format]
おわりに
Rustで書かれたPython用のリンター、フォーマッターであるRuffを使ってみました。
Pythonでの他のリンター、フォーマッターを使ったことがないので比較ができないのですが、ひとまずこれで慣れていこうと思います。