【Python】Ruffモジュールを使ってソースコードの解析と修正をする


投稿日 2023年5月1日 >> 更新日 2023年5月5日

概要

Ruffモジュールを使ったPythonリンター(解析ツール)を実行します。

RuffでサポートされているPythonリンターのプラグイン方法や、解析後の改善すべきコードを順番に修正していきます。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.10.0
使用ライブラリ ライセンス
ruff==0.0.263 MIT

Ruffモジュールについて

RuffモジュールはRustというプログラミング言語で開発されたモジュールで、他のPythonリンター(Python解析ツール)よりも高速で解析を行えるツールのようです。

Ruffモジュールは独自の解析手段を持っているわけではなく、多くのPythonリンターが統合されているモジュールです。

「統合」といっても、他のPythonリンターを1つのモジュールにしたのではなく、プラグインという形で好みのPythonリンターを設定して実行できるという柔軟性を持ったモジュールです。

デフォルトでは「pycodestyle」と「pyflakes」という解析ツールが設定されており、Ruffモジュールをインストールしてコマンドを実行するだけで既存ファイルのソースコードを解析することができます。

プラグインの方法はコマンドのオプションで解析ツールを指定するか、「pyproject.toml」という設定ファイルを作成して設定します。

様々なPythonリンターが使えるので、気になる方は公式ドキュメントをご覧ください。

Ruffの実装

pipでインスールします。

$ pip install ruff

インストールが完了したら、「help」コマンドでRuffモジュールの詳細情報を表示します。

$ ruff help
Ruff: An extremely fast Python linter.

Usage: ruff [OPTIONS] <COMMAND>

Commands:
  check   Run Ruff on the given files or directories (default)
  rule    Explain a rule
  config  List or describe the available configuration options
  linter  List all supported upstream linters
  clean   Clear any caches in the current directory and any subdirectories
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Log levels:
  -v, --verbose  Enable verbose logging
  -q, --quiet    Print lint violations, but nothing else
  -s, --silent   Disable all logging (but still exit with status code "1" upon detecting lint violations)

For help with a specific command, see: `ruff help <command>`.

Ruffの基本操作

以下の「sample.py」と言うファイルを準備して基本的な操作を行っていきます。

# sample.py

import ruff
from datetime import datetime
import sys
from datetime import timedelta
def day_count(num):
    number = num

    date = (datetime.now() + timedelta(days=num)).date().strftime('%Y年%m月%d日')
    if num > 0:
        return '{}日後は{}です'.format(num, date)
    elif num < 0:
        return '{}日前は{}です'.format(abs(num), date)
    else:
        return "今日は{}です".format(date)


if __name__ == '__main__':
    print(day_count(-1))

「sample.py」がruffの解析を通してどれ程スタイルが変化したか確認できるようにコピーしておきます。

$ cp sample.py cp_sample.py

Ruffモジュールで使用できるコマンドは「ruff help」によって確認することができます。

コードの解析を実行したい場合は、「ruff」もしくは「ruff check」コマンドを実行します。

$ ruff sample.py

$ ruff check sample.py

作業ディレクトリにある全てのファイルを解析したい場合はドット「.」を指定します。

$ ruff check .

RuffモジュールはPythonリンター(Pythonの解析ツール)を自由にプラグインすることができるので、コーディングスタイルのルールを甘くしたり厳しくしたりできます。

デフォルトのPythonリンターは「pycodestyle」と「pyflakes」が設定されていると公式ドキュメントにあります。

デフォルト設定であるPythonリンターのRuffを使って「sample.py」を解析してみます。

$ ruff check sample.py
sample.py:3:8: F401 [*] `ruff` imported but unused
sample.py:5:8: F401 [*] `sys` imported but unused
sample.py:8:5: F841 [*] Local variable `number` is assigned to but never used
Found 3 errors.
[*] 3 potentially fixable with the --fix option.

3件のエラーが見つかりました。

エラーログに記されている「F401」や「F841」と言うコードは「Pyflakes」という解析ツールでサポートされているルールです。

サポートされているルール一覧は以下をご覧ください。

「[*]」のあるエラーログは「--fix」オプションによって自動修正可能なことを表しています。

どの行やコードや修正されるかは、「--diff」オプションを付与すると確認することができます。

※「--fix」オプションは無効化される

$ ruff check sample.py --diff
--- sample.py
+++ sample.py
@@ -1,11 +1,8 @@
 # sample.py

-import ruff
 from datetime import datetime
-import sys
 from datetime import timedelta
 def day_count(num):
-    number = num

     date = (datetime.now() + timedelta(days=num)).date().strftime('%Y年%m月%d日')
     if num > 0:

Would fix 3 errors.

各行の先頭に「-」もしくは「+」が付いている場合は、その行のコードが修正されることになります。

「-」が付いている場合は削除され、「+」が付いている場合は修正して追加となります。

実際に自動修正を実行する場合は、「--fix」オプションを付与します。

$ ruff check sample.py --fix
Found 3 errors (3 fixed, 0 remaining).

上記結果は3件のエラーから3件修正されたとなりました。

修正前と修正後の「sample.py」を見てみます。

  • 修正前(cp_sample.py)
# cp_sample.py

import ruff
from datetime import datetime
import sys
from datetime import timedelta
def day_count(num):
    number = num

    date= (datetime.now() + timedelta(days=num)).date().strftime('%Y年%m月%d日')
    if num > 0:
        return '{}日後は{}です'.format(num, date)
    elif num < 0:
        return '{}日前は{}です'.format(abs(num), date)
    else:
        return "今日は{}です".format(date)


if __name__ == '__main__':
    print(day_count(-1))
  • 修正後(sample.py)
# sample.py

from datetime import datetime
from datetime import timedelta
def day_count(num):

    date = (datetime.now() + timedelta(days=num)).date().strftime('%Y年%m月%d日')
    if num > 0:
        return '{}日後は{}です'.format(num, date)
    elif num < 0:
        return '{}日前は{}です'.format(abs(num), date)
    else:
        return "今日は{}です".format(date)


if __name__ == '__main__':
    print(day_count(-1))

解析して自動修正を行った結果、使用されていないモジュールや変数が削除されて余分に処理が実行されないようになりました。

デフォルトで実行されるPythonリンターは上記のような結果となりましが、他のPythonリンターもインストールせずにプラグインして実行することができるのでコーディングルールを厳しくされたい方は試してみてください。

なお、「ruff help check」コマンドで「--fix」や「--diff」オプション以外のオプションを確認することができるのでよければご覧ください。

Pythonリンターのプラグイン

Ruffモジュールのデフォルトで設定されているPythonリンターは「pycodestyle」と「pyflakes」です。

Ruffで使用できるPythonリンターとルールの一覧は以下の公式ドキュメントにあります。

上記リンクのルール一覧表において、各ルールの説明が必要な場合は「ruff rule」コマンドで特定のルール説明を表示することができます。

※「F401」はpyflakesのコード

$ ruff rule F401
# unused-import (F401)

Derived from the **Pyflakes** linter.

Autofix is sometimes available.

Message formats:
* `{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability
* `{name}` imported but unused; consider adding to `__all__` or using a redundant alias
* `{name}` imported but unused

Pythonリンターをプラグインする方法は2つあって、1つ目が「ruff check」コマンドで解析を実行する際にオプションでPythonリンターを指定する方法で、2つ目が「pyproject.toml」に使用したいPythonリンターを定義するという方法です。

それぞれの方法で実行していきます。

「--select」オプションでプラグイン

オプションを使用してPythonリンターをプラグインするには、「ruff check」コマンドに「--select」オプションを渡します。

「--select」オプションに使用したいPythonリンターを指定することができます。

RuffがサポートしているPythonリンターを確認するには公式ドキュメントを見るか、「ruff linter」コマンドを実行して確認することできます。

$ ruff linter
   F Pyflakes
 E/W pycodestyle
 C90 mccabe
   I isort
   N pep8-naming
   D pydocstyle
  UP pyupgrade
 YTT flake8-2020
...

上記リストの左側はPythonリンターのコード名です。

「--select」オプションにリンターのコード名を指定するとプラグインされます。

以下はデフォルトで設定されている「pycodestyle」と「pyflakes」を指定した例です。

$ ruff check sample.py --select=E,F
sample.py:3:8: F401 [*] `ruff` imported but unused
sample.py:5:8: F401 [*] `sys` imported but unused
sample.py:8:5: F841 [*] Local variable `number` is assigned to but never used
Found 3 errors.
[*] 3 potentially fixable with the --fix option.

コード名ではなく、「ALL」と指定するとRuffがサポートしているPythonリンター全てをプラグインして実行することができます。

$ ruff check sample.py --select=ALL
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
sample.py:1:1: D100 Missing docstring in public module
sample.py:1:1: EXE002 The file is executable but no shebang is present
sample.py:3:1: I001 [*] Import block is un-sorted or un-formatted
sample.py:3:8: F401 [*] `ruff` imported but unused
sample.py:5:8: F401 [*] `sys` imported but unused
sample.py:7:5: ANN201 Missing return type annotation for public function `day_count`
sample.py:7:5: D103 Missing docstring in public function
sample.py:7:15: ANN001 Missing type annotation for function argument `num`
sample.py:8:5: F841 [*] Local variable `number` is assigned to but never used
sample.py:10:12: DTZ005 The use of `datetime.datetime.now()` without `tz` argument is not allowed
sample.py:10:66: Q000 [*] Single quotes found but double quotes preferred
sample.py:12:16: Q000 [*] Single quotes found but double quotes preferred
sample.py:12:16: UP032 [*] Use f-string instead of `format` call
sample.py:13:5: RET505 Unnecessary `elif` after `return` statement
sample.py:14:16: Q000 [*] Single quotes found but double quotes preferred
sample.py:14:16: UP032 [*] Use f-string instead of `format` call
sample.py:16:16: UP032 [*] Use f-string instead of `format` call
sample.py:19:16: Q000 [*] Single quotes found but double quotes preferred
sample.py:20:5: T201 `print` found
Found 19 errors.
[*] 11 potentially fixable with the --fix option.

「pyproject.toml」に定義してプラグイン

「.toml」は設定ファイルの1種で、「読みやすさ」をテーマに作られたフォーマットのようです。

モジュールの配布用パッケージを作成する際の設定ファイルとしても活用します。

プラグインされたいPythonリンターを定義する設定ファイル「pyproject.toml」をカレントディレクトリに作成します。

.
├── pyproject.toml # new
└── sample.py

「pyproject.toml」の中身は以下のように定義します。

[tool.ruff]
select = ["E", "F"]

上記はテーブル名「[tool.ruff]」、キー名「select」というフォーマットで定義しています。

「select」キーの中にRuffがサポートしているPythonリンターのコード名を設定しています。

サポートされているPythonリンターのコード名一覧は以下のコマンドで表示できます。

$ ruff linter

全てのPythonリンターで解析を行って修正をする

「sample.py」を例に、解析を行って修正していきます。

# sample.py

import ruff
from datetime import datetime
import sys
from datetime import timedelta
def day_count(num):
    number = num

    date = (datetime.now() + timedelta(days=num)).date().strftime('%Y年%m月%d日')
    if num > 0:
        return '{}日後は{}です'.format(num, date)
    elif num < 0:
        return '{}日前は{}です'.format(abs(num), date)
    else:
        return "今日は{}です".format(date)


if __name__ == '__main__':
    print(day_count(-1))

「--select」オプションを使ってサポートされている全てのPythonリンターで解析を実行します。

$ ruff check --select=ALL sample.py
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
sample.py:1:1: D100 Missing docstring in public module
sample.py:1:1: EXE002 The file is executable but no shebang is present
sample.py:3:1: I001 [*] Import block is un-sorted or un-formatted
sample.py:3:8: F401 [*] `ruff` imported but unused
sample.py:5:8: F401 [*] `sys` imported but unused
sample.py:7:5: ANN201 Missing return type annotation for public function `day_count`
sample.py:7:5: D103 Missing docstring in public function
sample.py:7:15: ANN001 Missing type annotation for function argument `num`
sample.py:8:5: F841 [*] Local variable `number` is assigned to but never used
sample.py:10:12: DTZ005 The use of `datetime.datetime.now()` without `tz` argument is not allowed
sample.py:10:66: Q000 [*] Single quotes found but double quotes preferred
sample.py:12:16: Q000 [*] Single quotes found but double quotes preferred
sample.py:12:16: UP032 [*] Use f-string instead of `format` call
sample.py:13:5: RET505 Unnecessary `elif` after `return` statement
sample.py:14:16: Q000 [*] Single quotes found but double quotes preferred
sample.py:14:16: UP032 [*] Use f-string instead of `format` call
sample.py:16:16: UP032 [*] Use f-string instead of `format` call
sample.py:19:16: Q000 [*] Single quotes found but double quotes preferred
sample.py:20:5: T201 `print` found
Found 19 errors.
[*] 11 potentially fixable with the --fix option.

「--fix」オプションを付与して自動修正をします。

$ ruff check --select=ALL sample.py --fix
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
sample.py:1:1: D100 Missing docstring in public module
sample.py:1:1: EXE002 The file is executable but no shebang is present
sample.py:6:5: ANN201 Missing return type annotation for public function `day_count`
sample.py:6:5: D103 Missing docstring in public function
sample.py:6:15: ANN001 Missing type annotation for function argument `num`
sample.py:8:12: DTZ005 The use of `datetime.datetime.now()` without `tz` argument is not allowed
sample.py:11:5: RET505 Unnecessary `elif` after `return` statement
sample.py:18:5: T201 `print` found
Found 19 errors (11 fixed, 8 remaining).

ログの「warning」部分においてはルールが対立してしまっているので、「D203」と「D212」は除外してしまいます。

f$ ruff check --select=ALL sample.py --ignore=D203,D212
sample.py:1:1: D100 Missing docstring in public module
sample.py:1:1: EXE002 The file is executable but no shebang is present
sample.py:6:5: ANN201 Missing return type annotation for public function `day_count`
sample.py:6:5: D103 Missing docstring in public function
sample.py:6:15: ANN001 Missing type annotation for function argument `num`
sample.py:8:12: DTZ005 The use of `datetime.datetime.now()` without `tz` argument is not allowed
sample.py:11:5: RET505 Unnecessary `elif` after `return` statement
sample.py:18:5: T201 `print` found
Found 8 errors.

エラーログ「sample.py:1:1: D100 Missing docstring in public module」を修正していきます。

内容は「公開モジュールにドキュメントがありません」とのことなので、ドキュメントを追記します。

# sample.py
"""日付確認用モジュール.""" # Add


from datetime import datetime, timedelta


def day_count(num):

    date = (datetime.now() + timedelta(days=num)).date().strftime("%Y年%m月%d日")
    if num > 0:
        return f"{num}日後は{date}です"
    elif num < 0:
        return f"{abs(num)}日前は{date}です"
    else:
        return f"今日は{date}です"


if __name__ == "__main__":
    print(day_count(-1))

エラーログ「sample.py:1:1: EXE002 The file is executable but no shebang is present」を修正していきます。

内容は「ファイルは実行できますが、シバンがありません」との事なので、ファイルの1行目に追記します。

「which」コマンドでPythonのソースコードが配置されている絶対パスを調べることができます。

※以下はpyenvによって作られたpython環境の絶対パス

$ which python3
/home/user/.pyenv/shims/python3

上記で得た絶対パスを「sample.py」の1行目に「#!」と一緒に追記します。

#!/home/user/.pyenv/shims/python3 # Add
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta


def day_count(num):

    date = (datetime.now() + timedelta(days=num)).date().strftime("%Y年%m月%d日")
    if num > 0:
        return f"{num}日後は{date}です"
    elif num < 0:
        return f"{abs(num)}日前は{date}です"
    else:
        return f"今日は{date}です"


if __name__ == "__main__":
    print(day_count(-1))

シバンを定義すると以下のようにPythonコマンド無しでPythonファイルを実行できるようになります。

$ ./sample.py
1日前は2023年05月02日です

エラーログ「sample.py:9:5: ANN201 Missing return type annotation for public function day_count」を修正していきます。

内容は「関数「day_count」のアノテーションがありません」との事なので、関数に追記していきます。

#!/home/user/.pyenv/shims/python3
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta


def day_count(num:int) -> str: # Fix

    date = (datetime.now() + timedelta(days=num)).date().strftime("%Y年%m月%d日")
    if num > 0:
        return f"{num}日後は{date}です"
    elif num < 0:
        return f"{abs(num)}日前は{date}です"
    else:
        return f"今日は{date}です"


if __name__ == "__main__":
    print(day_count(-1))

エラーログ「sample.py:9:5: D103 Missing docstring in public function」を修正していきます。

内容は「関数にドキュメントがありません」とのことなので、関数にドキュメントを追記します。

#!/home/user/.pyenv/shims/python3
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta


def day_count(num:int) -> str:
    """引数に整数を与えると日付を返す関数.""" # Add
    date = (datetime.now() + timedelta(days=num)).date().strftime("%Y年%m月%d日")
    if num > 0:
        return f"{num}日後は{date}です"
    elif num < 0:
        return f"{abs(num)}日前は{date}です"
    else:
        return f"今日は{date}です"


if __name__ == "__main__":
    print(day_count(-1))

エラーログ「sample.py:11:13: DTZ005 The use of datetime.datetime.now() without tz argument is not allowed」を修正していきます。

内容は「tz(タイムゾーン)引数無しでdatetime.datetime.now()を使用することは許可されていません」とのことなので、「datetime.datetime.now(tz)」の引数に値を渡します。

#!/home/user/.pyenv/shims/python3
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta, timezone # Add


def day_count(num:int) -> str:
    """引数に整数を与えると日付を返す関数."""
    date = (datetime.now(tz=timezone.utc) + timedelta(days=num)).date().strftime("%Y年%m月%d日") # Fix
    if num > 0:
        return f"{num}日後は{date}です"
    elif num < 0:
        return f"{abs(num)}日前は{date}です"
    else:
        return f"今日は{date}です"


if __name__ == "__main__":
    print(day_count(-1))

「datetime.datetime(tz=timezone.utc)」とした後に、新たなエラーログ「sample.py:11:88: E501 Line too long (96 > 88 characters)」が発生しました。

内容は「行の長さが88以上あります」とのことなので、変数を分散させます。

#!/home/user/.pyenv/shims/python3
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta, timezone


def day_count(num:int) -> str:
    """引数に整数を与えると日付を返す関数."""
    datetime_obj = datetime.now(tz=timezone.utc) + timedelta(days=num) # Fix
    date_obj = datetime_obj.date() # Add
    date = date_obj.strftime("%Y年%m月%d日") # Add
    if num > 0:
        return f"{num}日後は{date}です"
    elif num < 0:
        return f"{abs(num)}日前は{date}です"
    else:
        return f"今日は{date}です"


if __name__ == "__main__":
    print(day_count(-1))

エラーログ「sample.py:16:5: RET505 Unnecessary elif after return statement」を修正していきます。

内容は「return 文の後に不要な elif」とのことなので、「elif文」を「if文」に、「else文」を削除して「if文」に引っかからなければ「return」で返すというコードに書き換えます。

#!/home/user/.pyenv/shims/python3
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta, timezone


def day_count(num:int) -> str:
    """引数に整数を与えると日付を返す関数."""
    datetime_obj = datetime.now(tz=timezone.utc) + timedelta(days=num)
    date_obj = datetime_obj.date()
    date = date_obj.strftime("%Y年%m月%d日")
    if num > 0:
        return f"{num}日後は{date}です"
    if num < 0: # Fix
        return f"{abs(num)}日前は{date}です"
    return f"今日は{date}です" # Fix


if __name__ == "__main__":
    print(day_count(-1))

エラーログ「sample.py:22:5: T201 'print' found」を修正していきます。

内容は「print文が見つかりました」とのことです。

この挙動はPythonリンターの「flake8-print」のルールによるものからで、不必要に「print文」を使用してはいけないというエラーとWebサイトで調べて判断しました。

よって、「T201」コード自体を除外するか「print文」の行を削除するかです。

#!/home/user/.pyenv/shims/python3
# sample.py
"""日付確認用モジュール."""


from datetime import datetime, timedelta, timezone


def day_count(num:int) -> str:
    """引数に整数を与えると日付を返す関数."""
    datetime_obj = datetime.now(tz=timezone.utc) + timedelta(days=num)
    date_obj = datetime_obj.date()
    date = date_obj.strftime("%Y年%m月%d日")
    if num > 0:
        return f"{num}日後は{date}です"
    if num < 0:
        return f"{abs(num)}日前は{date}です"
    return f"今日は{date}です"


# if __name__ == "__main__": # Fix

以上でエラー箇所はすべて改善することができました。

修正前と比べると、シンプルでより分かりやすいコーディングになったかと思います。

  • 修正前(cp_sample.py)
# sample.py

import ruff
from datetime import datetime
import sys
from datetime import timedelta
def day_count(num):
    number = num

    date = (datetime.now() + timedelta(days=num)).date().strftime('%Y年%m月%d日')
    if num > 0:
        return '{}日後は{}です'.format(num, date)
    elif num < 0:
        return '{}日前は{}です'.format(abs(num), date)
    else:
        return "今日は{}です".format(date)


if __name__ == '__main__':
    print(day_count(-1))

最後までご覧いただきありがとうございました。