Vim と JupyterLab で Ruff を使う

新しい Python の静的コード解析ツール「[Ruff](https://docs.astral.sh/ruff/)」というものが出来たらしい。Ruff の紹介は [gihyo.jp の記事](https://gihyo.jp/article/2023/03/monthly-python-2303)が詳しいので譲るとして、ともかく Ruff は今後のデファクト・スタンダードになる感じがする。

個人的に感じた Ruff の良い所。

* 全部入り Linter
    * flake8, isort, mccabe, pycodestyle, pydocstyle, pylint, pyupgrade 全部に対応しているので、Ruff を入れるだけでこれら全部必要なくなる
* めちゃ速いらしい
    * JupyterLab で LSP を使うと、ファイルが大きくなった時に物凄く遅くなるので、期待したい
* pyproject.toml で全部設定できる
    * flake8 がいつまでたっても pyproject.toml に対応せずに困っていたので、これは嬉しい
        * [pyproject-flake8](https://github.com/csachs/pyproject-flake8) というのもあるのだが、これを使うと依存関係の問題が発生してしまう

# Formatter は Ruff か Black か

私は現時点（2023年12月）で Formatter としては Ruff ではなく [Black](https://github.com/psf/black) を推したい。その理由は３点。

* Formatter は Lint の fix をせずに本当に Format だけして欲しい
* 強制的に行分離させたい
* E3 に対応したい

（ひとつめ）

Ruff で Format すると、Lint でエラーとなっている箇所も自動的に直してしまう。
（デフォルトでは、自動的に変換しても問題ないものだけ）

そしてこれはこんな事態を引き起こす。

* とりあえず `import numpy as np` 等と書いておく
    * 後で numpy を使うものを書こうと思っていた
* うっかり Formatter を起動してしまう
* import 文が消えてしまったので、書き直し

これ、私は良くやってしまう。一旦ざっと書いて、綺麗にするのは Formatter 任せ。そして書いている途中でもとりあえず見栄えを確認したい。なんてことをやるので。

（ふたつめ）

以下の様に関数を書いたとする。

```python
def func(a: int, b:int,) -> None:
    return
```

そして Formatter を起動したら、これを以下の様に直して欲しい。

```python
def func(
    a: int,
    b: int,
) -> None:
    return
```

これがどうしても Ruff だと出来なかった。

（みっつめ）

def や class 間は２行開ける等の自動空行設定も Ruff は対応していなく、Black でないと出来ない。
（E301, E302 等の、いわゆる E3）

Ruff がもっとバージョンアップしたら、これらができるようになるのかもしれないが、とりあえずこれらの理由で、Formatter は Black を使いたい。幸い、Ruff と Black を併用しても（たぶん）問題無いだろう。

# 私の Ruff 設定

私は Ruff を以下のように設定している。自戒として、少し厳し目の設定にしているつもり。pyproject.toml での書式なので、ruff.toml を使う人はセクション名を読み替える必要がある。

```toml
[tool.black]
line-length = 80

[tool.ruff]
line-length = 80
target-version = "py310"

[tool.ruff.lint]
select = ["F", "E", "W", "C", "N", "UP", "PD", "NPY"]
ignore = ["E203"]

[tool.ruff.lint.mccabe]
max-complexity = 15
```

* Black は pyproject.toml に対応しているので、一緒に設定できるのは非常に喜ばしい
* Black や Ruff を、なるべくデフォルトの設定のまま使うことが、様々な争いを回避するために重要なんじゃないかと思う
* line-length は 80 一択。普通の大きさのターミナルで横幅に入り切ることが重要だと思う
* Python 3.10 系は既に security update だけのステータスになっていて（[ロードマップ](https://devguide.python.org/versions/)）、2026年10月にサポートが終わってしまうが、パッケージによってはまだ 3.11 に対応していないものもあるので、3.10 から抜け出せないでいる
    * [neologdn](https://github.com/ikegami-yukino/neologdn) のことだったんだけど、あれ、対応した？
* select の "N", "UP", "PD", "NPY" は完全に趣味の領域である。マゾなのかな？
* ignore は、Black を使うと避けられない E203 のみ。マゾなのかな？
* max complexity は昔ながらの 15 で。10 でもマジ無理です。ごめんなさい

# Vim で Ruff を使う

うん。VS Code でもなく、Neovim でもなく、Vim なんだ。老害。

私は以下の様に使い分けている。

* Completion, Reference 等：[vim-lsp](https://github.com/prabirshrestha/vim-lsp)
* Lint, Format：[ale](https://github.com/dense-analysis/ale)

## ale の Ruff 設定

ale の設定は簡単。vimrc に以下の設定をする。

```vim
let g:ale_linters = {
    \ 'python': ['ruff']
\ }
let g:ale_python_auto_poetry = 1
let g:ale_python_auto_virtualenv = 1
let g:ale_python_ruff_auto_poetry = 1
let g:ale_python_ruff_change_directory = 1
```

たぶんこれで大丈夫なはず。（無責任）（特に後ろの４行）

Formatter も Ruff を使いたいなら以下。（私は `black` と設定しているけど）

```
let g:ale_fixers = {
    \ 'python': ['ruff'],
\ }
```

## vim-lsp の Ruff 設定

ところが vim-lsp に Ruff を認識させるのが難しい。Official な方法は [ruff-lsp](https://github.com/astral-sh/ruff-lsp) を使う方らしいので、是非そちらを使いたいのだが、どうにも設定が上手く行かない。かと言って [vim-lsp-settings](https://github.com/mattn/vim-lsp-settings) を使うと、ruff-lsp 専用の venv を作ってしまう。そうじゃなくて今いる virtualenv の ruff を使いたいのだ！

仕方がないので、[python-lsp-server](https://github.com/python-lsp/python-lsp-server) 経由で、Ruff の python-lsp-server プラグインである [python-lsp-ruff](https://github.com/python-lsp/python-lsp-ruff) を使う。

virtualenv に ruff, python-lsp-ruff, python-lsp-server を入れて、vimrc には以下の設定をする。

```
let g:python_pylsp_prog = $VIRTUAL_ENV . '/bin/pylsp'
let g:python_ruff_prog = $VIRTUAL_ENV . '/bin/ruff'
augroup PythonLanguageServer
    if executable(g:python_pylsp_prog)
        autocmd!
        autocmd User lsp_setup call lsp#register_server({
            \ 'name': 'pylsp',
            \ 'cmd': {server_info -> [g:python_pylsp_prog]},
            \ 'whitelist': ['python'],
            \ 'workspace_config': {
                \ 'pylsp': {
                    \ 'plugins': {
                        \ 'ruff': {
                            \ 'enabled': v:true,
                            \ 'executable': g:python_ruff_prog
                        \ },
                        \ 'autopep8': {
                            \ 'enabled': v:false
                        \ },
                        \ 'yapf': {
                            \ 'enabled': v:false
                        \ }
                    \ }
                \ }
            \ }
        \ })
    endif
augroup END
```

Jedi 関係は使うので disable にしてはダメ。

python-lsp-ruff はデフォルトで pycodestyle, pyflakes, mccabe を disable にするので、明示的に disable にするのは autopep8 と yapf だけで良い筈。

## Vim でも Ruff で Format（Lint fix）する

Formatter は Black を使うようにしたが、Ruff で見つけた Lint Error を自動的に fix したい場合もあるだろう。その場合は vim-lsp 経由で Ruff の Format をすれば良い。

```vim
:LspCodeAction source.fixAll
```

この vim コマンドを実行すると、Ruff の Format が走る。keyboard shortcut 等を設定すれば、いつでも簡単に実行できるね！

# JupyterLab で Ruff を使う

この場合も Linter は２通りの設定方法が考えられる。

* jupyterlab-lsp + ruff-lsp + ruff
* jupyterlab-lsp + python-lsp-server + python-lsp-ruff + ruff

どちらが良いかを、設定ファイルの有無で考えてみる。

* jupyterlab-lsp + ruff-lsp + ruff
    * jupyterlab-lsp を ruff-lsp に対応させるため、`jupyter_server_config.d/ruff-lsp.json` 等の json ファイルを作り、設定させる必要がある
    * ruff-lsp が ruff の設定を認識するために、どこかに pyproject.toml か ruff.toml を配置する必要がある（たぶん）
* jupyterlab-lsp + python-lsp-server + python-lsp-ruff + ruff
    * jupyterlab-lsp は python-lsp-server (pylsp) に標準で対応しているため、特に設定する必要が無い
    * python-lsp-server は ruff の実行バイナリや pyproject.toml を自動的に検索してくれるので、普通にプロジェクトの pyproject.toml を認識してくれる
    * python-lsp-ruff の設定項目は（ruff-lsp より）多いので、それだけでそこそこの設定が出来てしまう

こうやって挙げてみると、断然に後者である。（ruff-lsp がバージョンアップすれば状況は変わるかもしれないが）

私は pipx に JupyterLab を入れる派なので、以下のコマンドでインストールする。

```shell
pipx install jupyterlab
pipx inject jupyterlab pip
pipx runpip jupyterlab install black ruff jupyterlab-lsp python-lsp-server python-lsp-ruff jupyterlab-code-formatter
```

普通に仮想環境等に入れる場合は、以下のコマンドで良いだろう。

```shell
pip install jupyterlab black ruff jupyterlab-lsp python-lsp-server python-lsp-ruff jupyterlab-code-formatter
```

ちなみに上記にしれっと入れてしまっているが、JupyterLab での Formatter の設定は [jupyterlab-code-formatter](https://github.com/ryantam626/jupyterlab_code_formatter) 一択である。

## jupyterlab-lsp の Ruff 設定

何も設定しなくてもインストールだけで何となく動いてしまうのだが（笑）。

残念ながら JupterLab の Settings Editor では設定できないので、json ファイルを直接編集することになる。

* JupyterLab を立ち上げて、メニューの Settings から Settings Editor を開く
* 左ペインから `Language Servers` の項目を選び、右ペインで "Available" となっている `pylsp` をクリック
* ”Settings to be passed to python-lsp-server (pylsp) in workspace/didChangeConfiguration notification.” の欄に `pycodestyle` と `pyflakes` があるので、`pyflakes` を選ぶ
* デフォルトの設定が json に丸っと吐き出される
* さらに右上の `JSON Settings Editor` をクリックすると、その内容を見ることができる

この json ファイルは `~/.jupyter/lab/user-settings/@jupyter-lsp/jupyterlab-lsp/plugin.jupyterlab-settings` という場所に置かれているので、それを直接エディタで編集する。
（もちろん JupyterLab の JSON Settings Editor で編集してもいいけど、ちょっと（かなり）やりにくい）

追加・変更した箇所だけ書き出してみる。

```diff
  {
      "language_servers": {
          "pylsp": {
              "serverSettings": {
                  "pylsp.configurationSources": ["pyflakes"],
+                 "pylsp.plugins.ruff.enabled": true,
+                 "pylsp.plugins.ruff.ignore": ["E203", "E402"],
~                 "pylsp.plugins.mccabe.enabled": false,
~                 "pylsp.plugins.pycodestyle.enabled": false,
~                 "pylsp.plugins.pyflakes.enabled": false,
~                 "pylsp.plugins.yapf.enabled": false,
              },
              "configuration": {
                  "pylsp.configurationSources": ["pycodestyle"],
+                 "pylsp.plugins.ruff.enabled": true,
+                 "pylsp.plugins.ruff.ignore": ["E203", "E402"],
~                 "pylsp.plugins.mccabe.enabled": false,
~                 "pylsp.plugins.pycodestyle.enabled": false,
~                 "pylsp.plugins.pyflakes.enabled": false,
~                 "pylsp.plugins.yapf.enabled": false,
              }
          }
      }
  }
```

先頭に `+` が付いている行が追加、先頭に `~` が付いている行が変更。

ここでは ignore の設定しかしてないけど、Ruff のその他の細かい設定は、プロジェクトの pyproject.toml にて行う。

先にも書いたが、python-lsp-server は実行バイナリや pyproject.toml を自動検出してくれるので、実行しているプロジェクトの pyproject.toml を設定すれば良い。

なぜここで ignore を設定しているのかは、次の章。

## 避けられない PEP8 違反と Ruff の ignore

Black や JupyterLab を使っていると、どうしても避けられない PEP8 違反が４つある。

* E203: Whitespace before ':'
    * 例）Black で `[:,:]` を Format すると `[:, :]` になってしまう
* E302: Expected 2 blank lines, found 0
    * 例）JupyterLab で各セルにひとつの関数を書き、ipynb をまるっとチェックした時、関数間の空行は０
* E402: Module level import not at top of file
    * 例）JupyterLab で `import matplotlib` の前に `%matplotlib inline` という [Magic Command](https://ipython.readthedocs.io/en/stable/interactive/magics.html) を宣言しないと、matplotlib の [backend](https://matplotlib.org/stable/users/explain/figure/backends.html) がちゃんと inline にならない
        * 今の matplotlib は賢くなっているので、Magic Command が無くても自動的に判断してくれるけど
* W503: Line break occurred before a binary operator
    * 例）Black は、長い式の時に２項演算子の前で改行する

しかし、E302, W503 は Ruff が対応してないので、そもそも ignore で設定できない。
（何が ignore で設定できるかは `ruff --ignore --help` で確認できる。）
（E302 は Black でフォローする）

jupyterlab-lsp の設定で ignore に追加したいのは、pyproject.toml（通常の Python ファイル）では無視する必要が無いけど、JupyterLab では無視したい項目なので、E402 だけを追加すれば良いということになる。
（けれどもまぁ念の為、E203 も入れてしまっているのだけれど）

## jupyterlab-code-formatter の Ruff 設定

jupyterlab-code-formatter は Ruff をサポートしているので、インストールするだけで、そしてちょっとした設定を追加するだけで動く。

* JupyterLab を立ち上げて、メニューの Settings から Settings Editor を開く
* 左ペインから `JupyterLab Code Formatter` を選ぶ
* 右ペインの "default formatter" で、python に `black` を設定する
    * ここで `ruff` を設定することもできるし、python-0 に `ruff`, python-1 に `black` を設定することで、ruff した後に black する、なんてこともできる
* 右ペインの一番下に "Ruff Config" があるので、そこの args に以下を追加する
    * `--select=F,E,W,N,UP,PD,NPY`
        * 修正したいルール（さすがに `C` は fix できない）
    * `--ignore=E203,E402`
        * 無視したい項目（上記参照）
    * `--target-version=py310`
        * お使いの Python バージョンに合わせて
    * `--unsafe-fixes`
        * いっそのこと、思い切ってやっちゃってくれ！（設定しなくて良い）
    * `--silent`
        * めちゃくちゃ重要

[jupyterlab-code-formatter の Ruff 対応実装](https://github.com/ryantam626/jupyterlab_code_formatter/blob/master/jupyterlab_code_formatter/formatters.py#L431-L475)を見ると、stderr に何か文字が吐き出されると、エラーが発生したと思って logger にメッセージを出して、出力は元のままにしている。
（そもそもこの logger は JupyterLab の logger でも何でも無いので、JupyterLab の Log Console を見ても何も出てこない！）

しかし jupyterlab-code-formatter が呼び出している ruff コマンド（`ruff --fix-only -`（最後のハイフンは subprocess から得られる stdin を入力とするという意味））は、何か変更すると stderr に "fix N Errors" という結果を御親切にも吐き出すので、これをエラーと勘違いしてしまう！

そこで `--silent` をつけて、この stderr への出力を抑制することで、まともに Ruff による Format が出来るようになる。

これを理解するのに丸一日かかったよ・・・

## JupyterLab でも Ruff で Format（Lint fix）する

python の default formatter として `black` を指定すると、jupyter-code-formatter のボタン（◯の斜めにチョロっと生えてるやつ）を押すと ipynb 全体を Black で Format する。

ほぼほぼこれを使うと思うのだが、JupyterLab の Edit メニューにも "Apply Black Formatter" と "Apply ruff Formatter" があり、これが実はセル毎に black, ruff で Format するものだ。

すなわち、セルを選び "Apply ruff Formatter" をすれば、Ruff で Format 出来る。（ipynb 全体は出来ないけど）

ちなみにわざわざメニューから呼び出さなくても、Command Palette（ctrl-shift-c）からも呼び出せる。

だから Formatter として Black を使う場合も、jupyterlab-code-formatter に Ruff の設定はしておいた方が良い。

# 最後に

それにしても何で jupyterlab-code-formatter さんは Black は "Apply Black Formatter" ってしてるのに、Ruff は "Apply ruff Formatter" ってしてるのかなぁ。思わずソースコードを書き換えて "Apply Ruff Formatter" って表示される様にしちゃったよ。