nikkie-ftnextの日記

イベントレポートや読書メモを発信

flake8 pluginの作りかた(抽象構文木ベースのpluginを作るまで)

はじめに

琴葉わん... nikkieです。

先日、琴葉ちゃんと一緒にいたい 世の中のPythonコードの関数の引数の型ヒントをよりPythonicにしたい一心で flake8-kotohaをリリースしました。

その中で分かったflake8 pluginの作りかた1をまとめます。

目次

第1の巨人:タイミー Product Team Blog

先人のアウトプットを参照して(=巨人の肩の上に乗りまくって)進めました。

flake8 pluginを初めて作るので、全体感を掴むのに役立ちました。

pluginには2種類ある

  • 抽象構文木を利用するplugin
  • 1行ずつ処理するplugin

flake8-kotohaは抽象構文木を利用するつもりだった2ので、前者について見ていきました。

  • プラグインの本体(TypeAPluginSampleクラス)
    • __init__()メソッド
    • run()メソッド
      • ジェネレータ
      • ast.NodeVisitorを継承したクラスをインスタンス化し、visit()メソッドを呼び出す
      • 本来ならvisitorに結果を溜め込んで結果に応じてエラーをレポート

  • pyproject.toml
    • (私はPoetryユーザではないので)[project]のdependenciesを指定しそうと読んだ
    • [project.entry-points."flake8.extension"]を指定する3
      • プラグインのクラスを指定(モジュール名:クラス名

以上で、pluginのプロジェクトをインストールしてflake8と叩くと、pluginが動きます!(Entry Pointsの好例!)
flake8 -hでインストールされたpluginを確認できました。

第2の巨人:anthony explains

タイミーさんの記事では抽象構文木の扱いの詳細までは記載がなかったので、別の先人のアウトプットを参照しました。
flake8のドキュメント Writing Plugins for Flake8 — flake8 7.1.0 documentation にあるビデオチュートリアルです。

ビデオチュートリアルとanthonyさんが作った拡張 flake8-2020 の両方を参考に、抽象構文木を扱う実装をしていきました。

Pythonの関数の呼び出しでfunc(**辞書)と書けるのですが、辞書のキーがハイフンを含むときにはエラーになります。
これがanthonyさんのpluginの題材でした

  • 8:02〜 プラグインのクラス
    • 1ファイルでプラグインを作っていっている
    • 2つの属性
      • name
      • version
    • 2つのメソッド
      • __init__()
      • run()
        • ジェネレータの返り値がtuple[int, int, str, Type[Any]](示していただき分かりやすい!)
        • flake8-2020の実装から、Visitorクラスはエラーパターンを内部に保持し4、Visitorが走査したあとで保持されたエラーパターンがyieldされる5ことを掴みました
  • 11:55〜 プラグインメタデータ
    • setup.cfgで示されたので、pyproject.tomlに読み替えました

俺の書いたpluginを見てくれ!

pyproject.toml(抜粋)
https://github.com/ftnext/kotoha-python-linter/blob/v0.1.0/pyproject.toml

[project]
dependencies = ["flake8"]

[project.entry-points."flake8.extension"]
KTH = "kotoha.plugins:Flake8KotohaPlugin"

src/kotoha/plugins.py(抜粋)
https://github.com/ftnext/kotoha-python-linter/blob/v0.1.0/src/kotoha/plugins.py

class Flake8KotohaPlugin:
    def run(self) -> Generator[tuple[int, int, str, Type[Any]], None, None]:
        checker = ArgumentConcreteTypeHintChecker()
        checker.visit(self._tree)

        for lineno, col_offset, message in checker.errors:
            yield (lineno, col_offset, message, type(self))

ArgumentConcreteTypeHintChecker6src/kotoha/core.py
https://github.com/ftnext/kotoha-python-linter/blob/v0.1.0/src/kotoha/core.py

class ArgumentConcreteTypeHintChecker(ast.NodeVisitor):
    def __init__(self) -> None:
        self.errors: list[tuple[LineNumber, ColumnOffset, ErrorMessage]] = []

終わりに

巨人の肩に乗って、抽象構文木ベースのflake8 pluginの作りかたを完全に理解しました。

  • pluginの実態はクラス
    • flake8が用意したクラスを継承といったことはしない
    • __init__()メソッドで抽象構文木を受け取る
    • run()メソッドがジェネレータ
      • Visitorクラスで抽象構文木を操作し、Visitorにたまったエラーパターンをyield
  • pyproject.toml
    • [project.entry-points."flake8.extension"]プラグインのクラスを指定

先人のアウトプットに感謝です。
ありがとうございます!

指定されたメソッドを用意したクラスを作るだけ!7
Entry Pointsってすごいなと思います(どう実装してるんだろう?)


  1. ワーディングは『冴えない彼女の育てかた』を意識しています。サエヒロならぬフレプラです。
  2. 抽象構文木を走査するスクリプト版がすでにありました。
  3. Entry Pointsの一例です!
  4. 一例ですが https://github.com/asottile-archive/flake8-2020/blob/v1.8.1/flake8_2020.py#L67-L69
  5. https://github.com/asottile-archive/flake8-2020/blob/v1.8.1/flake8_2020.py#L161-L162
  6. 記事を書いていて気づいたのですが、Pythonの用語集によると、argumentは実引数仮引数はparameterなんですよね。仮引数の型ヒントのpluginだからrename?(しかしtyping.Listのドキュメントはargumentを使っている)
  7. Entry Pointsではないですが、Sphinxの拡張(setup()関数を持ったモジュール)にも共通するような