読者です 読者をやめる 読者になる 読者になる

SideCI TechBlog

SideCIを作っているアクトキャットのエンジニアによる技術ブログです。


Flake8 のプラグイン機構についてのお話

こんにちは、 id:yykamei です。 今回の内容は、 SideCI でも利用している Flake8 についてです。 ちょうど 1 年ほど前に 弊社の記事 で取り上げた内容と 重複する部分もありますが、主に Flake8 のプラグインについて述べたいと 思います。

ここでは、 記事執筆時点で最新の Flake8 (バージョン 3.2.1) をベースに、 主に以下の内容を取り上げます。

  • Flake8 についておさらい
  • Flake8 のプラグインとは
  • Flake8 プラグインの作り方

Flake8 についておさらい

Flake8 は以下のツールのラッパーとして実装されています。

ここで 1 年前の記事には出てこなかった、 pycodestyle というツールが登場 します。これは pep8 がリネームされたものになります。 Flake8 を使っていれば、 pep8 が pycodestyle にリネームされたことを意識する必要はありませんが、 頭の片隅に入れておきましょう。

Flake8 の指摘は、特にプラグインなどをインストールしていなければ次のように 分けられます。

  • Exxx, Wxxx: pycodestyle の出力
    • PEP8 に準拠しないコードの指摘
  • Fxxx: PyFlakes の出力
    • 実行時エラーとなるコードの指摘
  • C901: McCabe
    • 循環的複雑度が高いコードの指摘

Flake8 のプラグインとは

標準の Flake8 は上述の通り 3 つのツールをラップしていますが、 McCabe のみ、 Flake8 の純粋なプラグインとして Flake8 から 利用されています (McCabe 自体は単体でも動作しますが、 McCabe 自身が Flake8 からも利用できるような実装になっています) 。

そもそも、 Flake8 のプラグインとは何を指すのでしょうか。

Flake8 では、 setuptools の Entry Points の仕組みを 利用して、サードパーティーのプラグインをロードしています。 setuptools は Python のパッケージングの仕組みを提供するライブラリーで、 Python 界隈で はデファクトとなっています。

重要なポイントは、 Flake8 のプラグインはあくまでも setuptools の 仕組みの上に乗ったものであり、決して独自のプラグイン機構を 備えているわけではないということです。

これによって、 Flake8 プラグイン製作者は標準の仕組みでパッケージングする だけで、容易にプラグインを制作することができます。 また、 Flake8 利用者には、プラグインを自分の環境にインストールするだけで プラグインを利用できるという利点があります。

なお、 Flake8 は、実行時に以下の名前空間の Entry Points を走査しています。

  • flake8.extension
    • コードチェックをする Entry Points のための名前空間
  • flake8.listen
    • Auto Correct をする Entry Points のための名前空間
  • flake8.report
    • 指摘の出力をカスタマイズする Entry Points のための名前空間

主な Flake8 プラグイン

  • hacking
  • flake8-docstrings
  • flake8-import-order

Flake8 プラグインの作り方

このように Flake8 では、プラグイン制作が容易に行える仕組みを備えていますので、 私もプラグインを作ってみたいと思います。

Flake8 プラグインの作成方法は Flake8 のドキュメント にあります。他にも、参考にした記事がございますので本記事下部にあるリンク をご参照ください。

環境準備

今回は、 Python 3.5.2 を利用します。まず、作業用の独立した環境を作り、 Flake8 をインストールします。

$ python -m venv venv
$ source venv/bin/activate
$ pip install flake8

Entry Points の定義

さっそくコードを作成します。まずは、以下の内容で、 setup.py というファ イルを作成します。重要なのは、 setup() に渡す entry_points の内容です。 これが上述の Entry Points になります。

entry_points には dict を渡します。また、ここでは、 Lint をしたいので、 'flake8.extension' をキーに渡します。 'flake8.extension' に対応する値は list で、 エラーコード = module:callable のような str を記述します。今回は X801 という エラーコードを定義したいと思います (なお、ここのエラーコードは現時点での実装では エラーコードではなくてもよいですが、 わかりやすさのためにエラーコードにしています) 。

# -*- coding: utf-8 -*-

from setuptools import find_packages, setup

setup(
    name='flake8-english-please',
    packages=find_packages(),
    entry_points={
        'flake8.extension': [
            'X801 = flake8_english_please.checker:english_checker',
        ],
    },
)

Lint の実装

続いて、 Lint を実際に行うコードを実装します。今回は english_checker という、コメント中に英語 (というより ASCII) 以外の文字を検出したときに エラーを吐き出す Lint を実装します (あまり実用的ではありませんが) 。

まずは、 Python パッケージのディレクトリー構造をつくります。

$ mkdir flake8_english_please
$ touch flake8_english_please/__init__.py

続いて、 flake8_english_please/checker.py というファイルを以下の内容で 作成します。

今回の Lint は物理行単位 physical_line でのチェックを行います。

コメント中に ASCII 以外の文字を検出したときに指摘する Lint
import tokenize

X801 = 'X801 This is a country where we speak English'


def english_checker(physical_line, tokens):
    for token_type, text, start, _, _ in tokens:
        if token_type != tokenize.COMMENT:
            continue
        for index, x in enumerate(text):
            if not ord(x) < 128:
                return (start[1] + index, X801)


english_checker.name = 'english_checker'
english_checker.version = '0.1.0'

補足ですが、 Lint となる実装は第一引数に以下のいずれかを指定する必要があります。

  • tree
    • Python の AST を受け取り、ファイル単位で Lint を行いたいときに利用
    • run というメソッドを定義した class で実装してもよい
      • その場合、コンストラクター (__init__) で tree を受け取るようにする
  • physical_line
    • 物理行ごとに Lint を行いたいときに利用 (今回の実装)
    • 関数しか定義できない
  • logical_line
    • 論理行ごとに Lint を行いたいときに利用
    • ジェネレーターしか定義できない

さらに、第二引数以降に Flake8 に対して欲しい情報を引数に設定して要求することができます。 詳細は こちら をご覧ください。

また、余談ですがこの例では指摘のメッセージを TrumpScript から拝借しています。

実装したプラグインを利用してみる

では、さっそくインストールしてみます。

$ python setup.py install

これでプラグインを利用する準備が整いました。

試しに、以下のようなファイル (example.py) を作成し、 flake8 を実行してみましょう。

# この関数は何かをします
#
# Hi! はい


def some():
    print('Some')
$ flake8 example.py

以下のような結果が出て、コマンド実行後の status code も 1 になっていれば 問題なく実装できています。

example.py:1:3: X801 This is a country where we speak English
example.py:3:7: X801 This is a country where we speak English

まとめ

今回は非常に簡単で実用性のないプラグインを実装しましたが、 Flake8 のプラグインが 容易に実装可能なことがおわかりいただけたかと思います。良いアイディアがあればぜひ 実装してみましょう。

参考にした記事