CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: flake8 のプラグインを書いてみる

flake8 は Python の linter の一種。 linter というのはソースコードを解析して問題がある箇所を見つけるツールの総称のことをいう。

flake8 の特徴としてプラグイン機構が備わっていることが挙げられる。 このプラグインは自分で自作できる。 つまり、独自の linter を flake8 に組み込むことができるということ。 今回は、そのプラグインを自分で書いてみることにしよう。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.4
BuildVersion:   15E65
$ python --version
Python 3.5.1

インストール

flake8 のインストールには pip を使う。

$ pip install flake8

インストールすると flake8 コマンドが使えるようになる。 バージョン情報と共にカッコ内に表示されている内容 (pyflakes や mccabe) はロードされたプラグインの一覧になっている。

$ flake8 --version
2.5.4 (pep8: 1.7.0, pyflakes: 1.0.0, mccabe: 0.4.0) CPython 3.5.1 on Darwin

使ってみる

まずは flake8 の基本的な使い方を再確認しておく。

次のようにシンプルなサンプルコードを用意する。 実はこれは一箇所だけ Python の標準的なコーディングスタイルである PEP8 に違反した箇所が含まれている。

$ cat << 'EOF' > helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

def main():
    print('Hello, World!')


if __name__ == '__main__':
    main()
EOF

このソースコードに対して flake8 を実行すると PEP8 に違反した箇所を見つけることができる。 ちなみに関数定義の前には二行の空行が必要、というルールに違反していた。

$ flake8 helloworld.py
helloworld.py:4:1: E302 expected 2 blank lines, found 1

プラグインを追加してみよう

次は flake8 にプラグインを足してみることにする。 サードパーティ製のプラグインがいくつかある中で、今回は flake8-todo プラグインを試してみる。 こちらも pip を使ってインストールしよう。

$ pip install flake8-todo

インストールすると flake8 コマンドのバージョン情報の中に flake8-todo が表示されるようになる。

$ flake8 --version
2.5.4 (pep8: 1.7.0, pyflakes: 1.0.0, flake8-todo: 0.4, mccabe: 0.4.0) CPython 3.5.1 on Darwin

この flake8-todo というプラグインはソースコードのコメントの中にある TODO や XXX といった文字列を検出できる。 これらのコメントは特に規定があるわけではないものの、デファクトスタンダードとしてよく使われている。 例えば TODO は文字通り「これからやること」、XXX は「問題があるけどそうしている理由がある」、FIXME は「問題があるので直すべきところ」などを表している。

動作を試すために TODO が含まれたソースコードを用意しよう。

$ cat << 'EOF' > helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def main():
    print('Hello, World!')  # TODO(momijiame): something


if __name__ == '__main__':
    main()
EOF

これを flake8-todo がインストールされた flake8 コマンドで実行すると TODO が残っている箇所を見つけることができる。

$ flake8 helloworld.py
helloworld.py:6:31: T000 Todo note found.

プラグインを作ってみる

使い方の説明がおわったところで、次は flake8 のプラグインを書いてみよう。 flake8 のプラグイン機構は setuptools という Python のデファクトスタンダードなパッケージング機構にある pkg_resources という仕組みを利用している。

今回作るプラグインは flake8-helloworld という名前にしよう。 プラグインは Python パッケージにする必要があるため、まずはそれ用のディレクトリを用意する。

$ mkdir flake8-helloworld

作業用ディレクトリの中にプラグインが含まれた Python モジュールを追加する。 名前は flake8_helloworld.py にした。 この中で定義している HelloWorldChecker というクラスがまずい箇所を見つけるチェッカーになる。

$ cat << 'EOF' > flake8-helloworld/flake8_helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import ast

__version__ = '0.0.1'


class HelloWorldChecker(object):
    name = 'helloworld'
    version = __version__

    def __init__(self, tree, filename):
        self.tree = tree

    def run(self):
        for child in ast.iter_child_nodes(self.tree):
            yield child.lineno, -1, 'X999 Hello, World!', type(self)
EOF

チェッカのインターフェースについて解説していく。 まず、チェッカはチェック対象のファイル毎にインスタンス化される。 そのとき init() メソッドにはソースコードの AST オブジェクトと、そのファイル名が渡される。 これは何故かというと flake8 のチェック対象は必ず Python モジュール (*.py) だからだ。

AST モジュールというのは Python モジュールを構文解析したオブジェクトのこと。 これについては以前このブログで詳しく書いているので、そちらも併せて参照のこと。 ちなみに今回作ったチェッカーは AST オブジェクトが持っている子要素毎にメッセージを返すという何ら実用性のないものになっている。

blog.amedama.jp

そして、実際にソースコードの内容をチェックするのが run() メソッドになっている。 このメソッドはジェネレータとして実装する。 ソースコードを解析して問題があったときは yield でその詳細を返す。 返す内容はタプルで (問題のある行番号, 問題のある列, 問題に関する詳細,問題を見つけたチェッカー) となっているようだ。

ソースコードを読んだところ、このインターフェースは flake8 ではなく、その内部で使っている pep8 というツールに由来しているようだった。 https://gitlab.com/pycqa/pep8/blob/1.7.0/pep8.py#L1521

問題に関する詳細に相当する文字列には少し注意点がある。 それぞれの問題には一意な識別子を含める必要があるらしい。 識別子のフォーマットは英字 1 文字 + 3 桁の数字になっている。 この詳細は以下に詳しく書かれていた。

http://flake8.readthedocs.org/en/latest/warnings.html

その上で run() メソッドでは「識別子 + 半角スペース + 問題を説明した文章」というフォーマットで返す必要がある。 先ほどのサンプルコードでは X999 という識別子を使って返していた。

次にパッケージングに必要なセットアップスクリプトを用意する。 ここでのポイントはふたつある。 まずひとつ目がパッケージング対象を py_modules 引数で 'flake8_helloworld' と指定しているところ。 そして、ふたつ目が肝心の flake8 のプラグインを指定するところだ。 これには entry_points 引数を使う。 具体的には flake8.extension というキーで flake8 のチェッカを指定する。 チェッカは「チェッカ名 = パッケージのパス:チェッカとして動作するクラス」として指定する。 こうしておくと flake8 が pkg_resources を使って、このチェッカを見つけることができる。

$ cat << 'EOF' > flake8-helloworld/setup.py
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
    setup(
        name='flake8-helloworld',
        version='0.0.1',
        zip_safe=False,
        py_modules=['flake8_helloworld'],
        install_requires=[
            'flake8',
        ],
        entry_points={
            'flake8.extension': [
                'flake8_helloworld = flake8_helloworld:HelloWorldChecker',
            ],
        },
    )


if __name__ == '__main__':
    main()
EOF

この時点でディレクトリは次のような状態になっている。

$ cd flake8-helloworld
$ tree
.
├── flake8_helloworld.py
└── setup.py

0 directories, 2 files

蛇足だけど tree コマンドは Mac OS X にデフォルトで入っていないので Homebrew を使ってインストールしよう。

$ brew install tree

これで準備が整ったので flake8-helloworld パッケージをインストールしよう。 インストールには先ほど作ったセットアップスクリプトを使う。

$ python setup.py install

上手くいくと flake8 のバージョン情報の中に「helloworld」が追加される。

$ flake8 --version
2.5.4 (pep8: 1.7.0, helloworld: 0.0.1, pyflakes: 1.0.0, flake8-todo: 0.4, mccabe: 0.4.0) CPython 3.5.1 on Darwin

試しに flake8 コマンドでセットアップスクリプトを処理してみよう。

$ flake8 setup.py
setup.py:3:0: X999 Hello, World!
setup.py:6:0: X999 Hello, World!
setup.py:23:0: X999 Hello, World!

自作のチェッカーが返した X999 のメッセージが表示されている。 ばっちりだ。

まとめ

今回は Python の linter のひとつである flake8 について見てきた。 flake8 の特徴のひとつとしてプラグイン機構が備わっている点が挙げられる。 このプラグインを実装するとオリジナルのルールに基いてソースコードをチェックすることができるようになる。

めでたしめでたし。