VSCodeでRuff/Mypyを導入しGitHub Actionsでも実行する

## はじめに
- 以前にPythonのリンターとフォーマッターは何が良いかな、と思って調べたとき、下記の構成がオススメされていてこちらを採用して運用してきました
    - [静的解析＆自動整形 flake8+black+isort+mypy](https://zenn.dev/zenizeni/books/a64578f98450c2/viewer/ac820f)

```
flake8
isort
black
mypy
```

- そんな中`Ruff`というツールが現在流行っていると目にしました
    - [新しい静的コード解析ツール「Ruff」をご紹介](https://gihyo.jp/article/2023/03/monthly-python-2303)
- これは簡単にいうと他のツールの比較して10~100倍高速で、リンターとフォーマッターが統合されたツールです

>https://docs.astral.sh/ruff/
>![](https://i.imgur.com/ZXh0Ao9.jpeg)
> (翻訳&一部抜粋)
> ⚡️ 既存のリンター (Flake8 など) やフォーマッタ (Black など) よりも 10 ～ 100 倍高速
> ⚖️ Flake8、 isort 、 Blackとのドロップイン同等性
> 🔧 自動エラー修正のサポートを修正 (例: 未使用のインポートを自動的に削除)
> 📏 flake8-bugbear などの人気のある Flake8 プラグインのネイティブ再実装を備えた800を超える組み込みルール
>...
>Ruff を使用すると、Flake8 (および数十のプラグイン)、 Black、isort、 pydocstyle、pyupgrade、 autoflakeなどを置き換えることができ...

### 静的解析ツールの置き換え

- 先述のツール類を、図のようにRuffで置き換えることができます
- 利用するツールがRuffにまとまって話が簡単になるのが嬉しく、より高速で動くというのが良いと思い、今回採用することにしました

![42059889-18BB-4AE4-94F7-15AA81B26F79](https://i.imgur.com/HzDONor.png)

- なお`mypy`に関してはRuffには型チェックの機能がないので、組み合わせての利用が推奨されています

>https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-pylint
>
>Ruff を Mypy、Pyright、または Pyre などの型チェッカーと組み合わせて使用​​することをお勧めします。Ruff は lint 違反に関するより迅速なフィードバックを提供し、型チェッカーは型エラーに関するより詳細なフィードバックを提供します。

### VSCodeへのRuffとmypyの導入
- エディタにVSCodeを利用しているので、それぞれの拡張機能をインストールして利用します
    - [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff)
    - [Mypy Type Checker](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker)
- 現在(2024-04-12)拡張機能に搭載されているバージョンは以下の通りです
  - もし今後バージョンが新しくなった際に、設定のフォーマットが変わっている可能性もあるのでご留意ください

```
ruff==0.3.1
mypy=1.8.0
```

- 続いて`<プロジェクトのルートディレクトリ>/.vscode/settings.json`を作成し、拡張機能の設定を行います
  - mypyとRuffのそれぞれの設定ファイルに`config=pyproject.toml`を指定
  - ファイル保存時にコードをRuffでフォーマットする

```jsonc
{
    //////////////
    // mypyの設定
    //////////////

    // 設定ファイルの指定
    "mypy-type-checker.args": [
        "--config=pyproject.toml"
    ],

    //////////////
    // Ruffの設定
    //////////////

    // refs: https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff
    "[python]": {
        // 保存時にコードを整理
        "editor.formatOnSave": true,
        // デフォルトのformatterにruffを指定
        "editor.defaultFormatter": "charliermarsh.ruff",
        "editor.codeActionsOnSave": {
            // 保存時にimportを整理
            "source.organizeImports": "explicit"
        }
    },
    // ruffの設定は下記ファイルから読み込み(指定が無い場合でも自動で探索されるが明示しておく)
    "ruff.format.args": [
        "--config=pyproject.toml"
    ],
}
```

- 続いてmypyの設定を`pyproject.toml`に追記します
    - 下記はあくまで一例ですので、各自必要となる設定を記載してください
- 最初はルールを最小限にしておいて、警告を見ながら緩和させたいルールを追加してくのが良いと思います

```toml
############
# mypyの設定
############

# https://mypy.readthedocs.io/en/stable/config_file.html

[tool.mypy]
python_version = "3.12"
show_error_context = true  # エラー時のメッセージを詳細表示
show_column_numbers = true  # エラー発生箇所の行数/列数を表示
disallow_untyped_defs = true  # 関数定義の引数/戻り値に型アノテーション必須
no_implicit_optional = true  # デフォルト引数に None を取る場合型アノテーションに Optional 必須
check_untyped_defs = true  # 型注釈がない関数やメソッドに対して型チェックを行う
warn_redundant_casts = true  # 冗長なキャストに警告

[[tool.mypy.overrides]]
# サードパーティの[import-untyped]を無視する
module = [
    'requests/*',
    'psutil',
]
ignore_missing_imports = true
```

- Ruffの設定を`pyproject.toml`に追記します
- こちらも最初はルールを最小限にしておいて、警告を見ながら緩和させたいルールを追加してくのが良いと思います

```toml
############
# Ruffの設定
############

[tool.ruff]
line-length = 120

# Option Settings
## https://gihyo.jp/article/2023/03/monthly-python-2303
## 略称一覧: https://pypi.org/project/ruff/0.0.242/#supported-rule

## select: 指定したルールをチェックの対象とする
lint.select = ["ALL"]
## ignore: 指定したルールをチェックの対象としない
lint.ignore = [
    "D", # Docstringを中途半端にしか書いていないので、除外する
]

# Assume Python 3.12
target-version = "py312"

[tool.ruff.lint.per-file-ignores]
# ファイル毎に無効とする設定

# https://docs.astral.sh/ruff/settings/#lint_per-file-ignores
"*.py" = [
    "ANN101", # selfの型を省略するため
    "ANN102", # clsの型を省略するため
    "COM812", # 末尾のカンマを必須としない
    "ERA001", # コメントアウト文を許可
    "FBT001", # 関数の引数にbooleanを許可(TODO:見直し)
    "FBT002", # 関数の引数にbooleanを許可(TODO:見直し)
    "INP001", # __init__.pyを必須としない
    "PT009", # assertEqualなどを使うため
    "PTH207", # glob.globを許可
    "S311", # randomを許可
    "S603", # Shell無しのsubprocessを許可
    "T201", # print文を許可するため
    "TRY002", # 標準のExceptionの使用を許可
]

[tool.ruff.lint.pydocstyle]
# pydocstyleを無視しているのでこの設定は無意味だが、
# いずれdocstringのスタイルを統一するので設定だけしておく
convention = "google"
```

### GitHub Actions上での実行
- GitHub ActionsのPR時にRuffとmypyで静的解析を行いたいので、下記の通り`.github/workflows/lint-python.yml`を作成します
- また今回VSCode拡張機能とCI間でツールのバージョンを統一してはいませんが、統一する場合は`pip install ruff=0.3.1`のようにCI上のバージョンを指定すると良いです

```yml
name: Lint Check

on:
  pull_request:
  workflow_dispatch:

jobs:
  run-lint:
    runs-on: ubuntu-latest
    name: Run Lint
    steps:
      - name: Check out source repository
        uses: actions/checkout@v4
      - name: Set up Python environment
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      # キャッシュの読み込み
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-
      # 依存関係のインストール
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip

      # Ruffの実行
      # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#using-ruff-to-lint-code
      - name: Lint with Ruff
        run: |
          pip install ruff
          ruff check . --output-format=github

      # mypyの実行
      - name: Lint with Mypy
        run: |
          pip install mypy
          mypy . --config-file pyproject.toml --ignore-missing-imports --no-namespace-packages
```

- mypyの引数に関して…
- `--ignore-missing-imports`:
  - これは今回はCI上で外部パッケージをインポートしないため、パッケージ類が見つからずエラーとなってしまうのでこれを無視するため
- `--no-namespace-packages`:
    - ローカルではプロジェクトの設定でパッケージのディレクトリを指定しており、mypyでエラーとはならないのですがCIではその設定が読み込まれないため

```toml
[tool.hatch.build.targets.wheel]
packages = ["src"]
```

## VSCodeの拡張機能とローカルのRuffの衝突の解消
- ある日VSCodeの拡張機能のRuffが動作しなくなった
- ローカルにもpipでRuffを入れているが、どうやらこれが悪さをして拡張機能が動いていないようだった

```shell
# VSCodeのOUTPUT
2024-08-12 22:38:30.619 [warning] The following settings are not supported with the native server: ["ruff.format.args"]
# ↓を見る限りパスがおかしそう
2024-08-12 22:38:30.825 [info] Using environment executable: /Users/<user>/.pyenv/shims/ruff
2024-08-12 22:39:05.994 [info] Server: Stop requested
```

- `.vscode/settings.json`に下記を記載し明示的にruffのバイナリを指定したところ解消した
    - 参考: [\[VSCode\] Ruffの設定メモ](https://qiita.com/BlueSilverCat/items/f44a0253609673c5fdbb)

```jsonc
{
    // 拡張機能のRuffを優先する設定
    "ruff.importStrategy": "useBundled",
}
```

## まとめ
- いくつかのリンターとフォーマッターを、Ruffにまとめることができました！
- ツールが少なくなった分、設定ファイルの管理がしやすくなりましたし、Ruffの動作自体が高速で快適で採用して良かったな〜と思います
- 静的解析ツールも移り変わりが激しいのですが、Ruffが決定版になればいいな、と期待をしています

---

### 参考
- [【Python】Linterとして選ばれたのは「Ruff」でした](https://qiita.com/naoya_s/items/0ddfcba5388783193f41)
- [Pythonの Ruff \(linter\) でコード整形もできるようになりました](https://qiita.com/ciscorn/items/bf78b7ad8e0e332f891b)
- [新しい静的コード解析ツール「Ruff」をご紹介](https://gihyo.jp/article/2023/03/monthly-python-2303#gh6DBDo6S4)
- [Ruff with VSCodeのススメ](https://qiita.com/taskforce_Hisui/items/9b89c5fad5ad327af9db)
- [Pythonの静的解析ツール"Ruff"を導入した話とおすすめの導入方法](https://qiita.com/yuji38kwmt/items/63e82126076204923520)