👌

Pythonライブラリ開発にpysenを導入してみた

2021/09/25に公開

OSS の Python ライブラリ開発をする機会があり、コードを綺麗に保つ環境を整えた方が良いだろうということで、 pysen を導入してみました。
この記事では、 pysen をしばらく使ってみた感想を書きました。
https://github.com/pfnet/pysen

Pythonのlinterとformatter

Python Enhancement Proposal(PEP) という Python に関するドキュメント集があり、そのうち、8 番目の PEP 8 が Style Guide for Python Code について書かれています。コーディング規約はこの PEP 8 を基準に考えられているそうです。
https://www.python.org/dev/peps/

linter

このコーディング規約を支援するツールあり、linter はコーディング規約を守っているかチェックするツールです。
代表的なものとしては次のようなものがあります。
上位 4 つはコーディングスタイルや論理的なエラー[1]を検出するのに対し、mypy は PEP 484, PEP 526 での型ヒントに関するチェックを行います。

  • pycodestyle (旧 pep8)
  • pyflake
  • flake8 (pycodestyle + pyflake のラッパー)
  • pylint
  • mypy (PEP 484, PEP 526 型ヒント)

formatter

formatter はコーディング規約を満たすようにコードを整えるツールです。
上位 3 つはコーディングスタイルを自動的に修正し、 isort は import の順番のみを修正するツールとなっています。

  • autopep8
  • yapf
  • black
  • isort (import の順番を修正)

linterとformatterの選び方は…

どれを使えばよいかについては、特に決まりはありません。大事なことは linter や formatter を利用してコードを整えておくことです。

どのツールも PEP 8 を基準にしていますが、それぞれで独自のコーディング規約を持っているものがあり、好みが分かれるポイントでもあるようです。
個人的には、black のフォーマットに flake8 を合わせる形が好みです。black と合わないところは flake8 の Warning を出ないように設定する感じです。pysen を使い始めたことをきっかけに、これらに加えて isort + mypy を使いはじめました。

コマンドの使い方、configの書き方…

flake8 と black を組み合わせて利用する際に抑制する Warning は何か、その設定をどう書くのかを調べるだけでも結構がかかります。
flake8 の場合は、今なら .flake8, setup.cfg, pyproject.toml などの候補があり、プロジェクトによって様々です[2]

linter、formatter の実行時にどのような引数を与えると良いのかについても、知らなくても出来るのであれば、導入コストが下がって良さそうです。

という背景でpysenは作られているらしい

設定項目が散乱しがちな linter と formatter を一元管理して導入コストを抑えられるところに共感し、今回導入するきっかけになりました。


https://tech.preferred.jp/ja/blog/pysen-is-the-new-sempai/

使い方

基本的な使い方

README や紹介記事内でも触れられていますが、ここでも簡単に紹介しておきます。

pysen は flake8 + isort + mypy + black を利用し、pip install 時に同時に取得することが出来ます。

pip install "pysen[lint]"

次に、pysen 用の設定を pyproject.toml に記述しておきます。この内容は README に記載されていたものです。

pyproject.toml
[tool.pysen]
version = "0.9"

[tool.pysen.lint]
enable_black = true
enable_flake8 = true
enable_isort = true
enable_mypy = true
mypy_preset = "strict"
line_length = 88
py_version = "py37"
[[tool.pysen.lint.mypy_targets]]
  paths = ["."]

pyproject.toml を配置したら、後は以下のコマンドで formatter や linter を実行できます。

pysen run format
pysen run lint

CI

GitHub Actions であれば以下のように 1 コマンドで linter によるチェックを統合出来ます。
format 後に差分が発生したり、lint error が発生した場合は error code で 1 を返してくれるので、そのまま書けば OK です。

- name: Check format & lint
  run: pysen run lint

この例ではエラーは発生していませんが、実行結果がとても分かりやすく気に入っています。

他のエディタとの連携

さっきのコンフィグはあくまでも pysen 用でした。VSCode など、他のエディタで使うには各 linter 用の設定が必要になります。
これは以下のコマンドで解決できます。pysen で管理している設定を pyproject.tomlsetup.cfg に出力出来るので、VSCode であれば勝手に読み込んでいい感じにしてくれました。

pysen generate .

また、VSCode 拡張機能として pysen-vscode も用意されており、flake8 や mypy の実行結果をエディタ上に表示することが出来るようです。
Vim や Emacs への統合方法についても README で説明されていました。

https://marketplace.visualstudio.com/items?itemName=bonprosoft.pysen-vscode

使う時に気にしたこと

今回記事を書こうと思ったきっかけがここからの内容です。
pysen は記事執筆日(2021/9/25)時点での最新バージョンであった 0.9.1 を対象としています。

追記: バージョン 0.10.0 のリリースに伴い、 GitHub リポジトリの README.md が更新され、 Frequently Asked Questions のセクションが追加されたようです。
Q. pysen seems to ignore some files. の項目もありますね。(ありがとうございます)

Git 管理されているファイルがflake8やformatterのターゲットとなる

コミット前に format を確認するためにコマンドを実行するわけですが、なぜかフォーマットのターゲットとならないことがありました。
この例では、run_files で直接ファイル指定していますが、それでも Skip されてしまっています。

少し、コードを読み込んでみました。 Skip されているところはこの 42 行目のようです。


https://github.com/pfnet/pysen/blob/be7666856cf99f07fc77c9d5fc0a9385498505e6/pysen/lint_command.py#L42

更に読んでいくと、 Git で追跡されているかどうかをチェックしている部分が見つかりました。


https://github.com/pfnet/pysen/blob/be7666856cf99f07fc77c9d5fc0a9385498505e6/pysen/source.py#L151

結果としては、 flake8 や formatter は Git 管理下にあるファイルのみがターゲットとなる、が正解でした。関係ないファイルの lint エラーが表示されなくなり便利なのですが、気が付きにくい仕様だったので、ドキュメントに書いておいてくれると嬉しいです。

とりあえず、flake8 の lint、isort や black の format のターゲットになっていないような気がしたら、git addしているか確認してみると良さそうです[3]

ただし、mypyは別。

ただし、mypy の lint は Git で管理されていても、ターゲットにはならないらしく、別に記述する必要がありました。

[[tool.pysen.lint.mypy_targets]]
  paths = [
    ".",
    "./tests/"
  ]

pysen で実行される mypy の正確な仕様を確認したわけではありませんが、経験的には以下のようなルールがありました。ドキュメントが無く、実験から推測した結果で、正確ではない可能性があります。

  1. カレントディレクトリ内のファイル
  2. Python ライブラリのモジュールとして認識出来るディレクトリ(__init__.pyが配置されているディレクトリ)内のファイル
  3. ./testsディレクトリ内のファイル

また、どちらもサブディレクトリは対象にはなりません。例えば、./tests/package1/にあるファイルは対象になりません。もし、そのサブディレクトリに __init__.py があれば 2. のルールで対象になります。

mypy用のpyiファイルに対するlint

mypy 用に型ヒントを記述する *.pyi ファイルに対しては、 無視してほしい Warning が *.py とは異なります。
そこで、 flake8 の per-file-ignores を利用して、以下のように記述することで実現出来ます。

[flake8]
per-file-ignores =
  *.py: E203,E231,E501,W503
  *.pyi: E301,E302,E305,E701,E704,E741

しかし、pysen では ignore にしか対応していません。
mypy を利用するにあたって、こういった需要は必ず出てきます。そこで、pysen を導入しているリポジトリを調査したところ、pysen 向けの plugin を書いて利用していることが見つかりました。


https://github.com/rospypi/ros_stubs/blob/master/assets/pysen_plugins/stub_flake8.py

VSCodeとの連携

per-file-ignores の設定

先程の per-file-ignores の plugin は、まだ setup.cfg に自動で書き出すことは出来ないようでした。正確には、 ignore の項目として出力されます。そのため、自分で書き直す必要があります。

一度、pysen generate . で設定を書き出した後、以下のように修正しました。

 [flake8]
-ignore = E203,E231,E501,W503
+# ignore = E203,E231,E501,W503
+
+per-file-ignores =
+  *.py: E203,E231,E501,W503
+  *.pyi: E301,E302,E305,E701,E704,E741

VSCode 向けの拡張機能として pysen-vscode が用意されていると紹介しました。しかし、 pysen-vscode は VSCode の Python 拡張機能である ms-python と競合するために、ms-python を無効にする必要があります。
これについては、他の方も言及されていました[4][5]

ms-python を無効にするということは、インテリセンスやデバッグ機能が使えなくなるということなので、利用は厳しいように感じました。今はコマンドラインからの実行だけで pysen を利用しています。
普段は ms-python を利用し、あるプロジェクトだけ pysen-vscode を利用するという方法を一応考えてみたので紹介しておきます。実現方法としては、あるワークスペースに対して拡張機能を無効/有効にする機能を利用します。

  1. まず、pysen-vscode の拡張機能のページから、グローバルで無効化する。
  2. pysen-vscode を使いたいプロジェクトを VSCode で開き、名前をつけてワークスペースを保存から、ワークスペースを作成する。
  3. 作成したワークスペースを開き、pysen-vscode をワークスペースに対して有効化、ms-python をワークスペースに対して無効化する。

次回からは、ディレクトリを開く代わりにワークスペースを開くようにすれば OK です。また、ms-python を無効にしたことで、 Python インタプリタを VSCode から変更出来なくなります。
指定したい場合は、使いたい Python 仮想環境を事前に activate した状態でコマンドラインから VSCode を開き、そこからワークスペースを開き直すようにすれば出来ました。

source ./venv/bin/activate
code .

debugログがログしていない

今回の問題を解決するために、pysen の挙動を確認しようと思い、debug ログを出力しようとしました。しかし、肝心の部分が見切れてしまっています。
省略しないで…みたいな気持ちになりました。

再びコードを眺めてみました。


https://github.com/pfnet/pysen/blob/be7666856cf99f07fc77c9d5fc0a9385498505e6/pysen/reporter.py#L83


https://github.com/pfnet/pysen/blob/be7666856cf99f07fc77c9d5fc0a9385498505e6/pysen/reporter.py#L19

どうやら、文字数制限がつけられているようでした。debug ログぐらい全部出力したいのですが、何か問題でもあったのでしょうか…[6]

おわり

導入はめちゃめちゃ簡単ですし、実行結果がまとまっていて見やすいところがとても気に入っていて、導入していないプロジェクトにはおすすめしたいツールだと感じました。
ですが、今回のような細かい部分が惜しいです。結局 pysen の挙動を理解するコストが出てしまったので、pysen を使って linter や formatter の知識が増えれば、それぞれのコマンドを呼び出す方向への切り替えも検討しています。その方が PFN 外部の人が使う面では、ドキュメントや参考プロジェクトが多く、楽そうと感じてしまいました...。
これから先の改善を楽しみにしています。

脚注
  1. import されたが利用されていないライブラリの検出など。 ↩︎

  2. どこに書いても同じように利用出来るので、どこに書いても正解ではある。 ↩︎

  3. この記事を書くために挙動を確認していたときにもまたハマって辛かった ↩︎

  4. https://zenn.dev/tktcorporation/articles/85003c3975fd55#拡張機能のアンインストール ↩︎

  5. https://zenn.dev/link/comments/1fb28b99217645 ↩︎

  6. あまりにも長いログが出力されるとか…?それでも見たいものは見たい。 ↩︎

GitHubで編集を提案

Discussion