2011年8月23日

Waf を使って構文チェックしてからコピーする

インタープリタ型のプログラミング言語では、 ソースコードを記述したファイルをそのまま実行できます。 小さめのファイルなら簡単に動作を確認できて非常に便利です。 しかし、モジュールを区切って管理するようになると複数のファイルに依存関係が発生し、 こまめにテストしながら開発する必要があります。

ひとくちにテストと言っても様々なレベルが考えられますが、 ここでは、構文だけをテストします。 原理は同じですから、同様にして単体テストやドキュメントテストも実行できるようになります。 設定方法を工夫すれば結合テストもできるようになるはずですね。

Python で pep8

Python には PEP 8 - Style Guide for Python Code - という約束があり、 これに沿っているかを確認するために pep8 というツールがあります。 インデントは空白4文字になっているか、1行が80文字以上になっていないか、 クラス定義の前に2行の空行があるか、エンコーディングは何かといったことを確認してくれます。

PyPI にモジュールがアップされていますので、簡単にインストールできます。

$ pip install pep8

Python スクリプトを引数に渡してあげると次のようになります。

$ cat <<EOF >pep8-test.py
if True is False:
  print "Bad world."

class True(object):
  pass

False = True
print type(False), True is False
EOF

$ python pep8-test.py
<type 'type'> True

$ pep8 pep8-test.py
pep8-test.py:2:3: E111 indentation is not a multiple of four
pep8-test.py:4:1: E302 expected 2 blank lines, found 1

インデントが4で揃っていないこと、空行が2行あるべき部分に1行しか見つからないことが報告されます。

コマンドライン引数を渡すことで、 doctest を実行したり、無視するエラーレベルを調整できます。 --show-source--show-pep8 オプションを使うと詳細な説明も表示されますので、 コードスタイルに慣れておらずソースコードが小さいうちは、これらを付けた方が良いかもしれません。 (記述量が多くなってくるとエラーが埋もれてしまうのと、エラー内容を見ればだいたい分かるので、 しばらく使うといらなくなる感じ。)

Waf で pep8

wscript を用意して Waf で実行できるようにします。 先ほどの pep8-test.py をそのまま使い、構文エラーがなくなったら _build ディレクトリにコピーできるようにします。

ツールとパスの確認

まずは wscript の大枠を整えます。

APPNAME = 'pep8-test'
VERSION = '1.0.0'

top = '.'
out = '_build'


def configure(ctx):
    ctx.find_program('pep8')


def build(bld):
    f = bld.path.find_resource('pep8-test.py')
    print "f                     : %s" % f
    print "f.abspath()           : %s" % f.abspath()
    print "f.get_src().abspath() : %s" % f.get_src().abspath()
    print "f.get_bld().abspath() : %s" % f.get_bld().abspath()

configurepep8 コマンドが存在しているかを確認します。 コマンドが見つからない場合は次のようなエラーになります。

$ waf configure
Setting top to                           : /private/tmp/pep8-test
Setting out to                           : /private/tmp/pep8-test/_build
Checking for program pep8                : not found
Could not find the program pep8
(complete log in /private/tmp/pep8-test/_build/config.log)

探索しているパスの一覧はログファイル (_build/config.log) に記載されています。 環境変数が思い通りに設定できていない場合はこのファイルを確認します。

pep8 をインストールすると次のようにチェックされます。

$ waf configure
Setting top to                           : /private/tmp/pep8-test
Setting out to                           : /private/tmp/pep8-test/_build
Checking for program pep8                : /private/tmp/pep8-test/bin/pep8
'configure' finished successfully (0.004s)

次に build ターゲットを実行します。 ターゲットとするファイルを find_resource で取得し、いくつかのパス表記を確認しています。

$ waf build
Waf: Entering directory `/private/tmp/pep8-test/_build'
f                     : pep8-test.py
f.abspath()           : /private/tmp/pep8-test/pep8-test.py
f.get_src().abspath() : /private/tmp/pep8-test/pep8-test.py
f.get_bld().abspath() : /private/tmp/pep8-test/_build/pep8-test.py
Waf: Leaving directory `/private/tmp/pep8-test/_build'
'build' finished successfully (0.002s)

get_src()get_bld() でノードが異なることが分かりますね。

pep8 と cp

pep8 で構文をチェックして cp でビルドディレクトリにコピーするには、 build ターゲットを次のように書き換えます。

def build(bld):
    f = bld.path.find_resource('pep8-test.py')
    bld(rule='pep8 ${SRC}', source=f)
    bld(rule='cp ${SRC} ${TGT}', source=f.get_src(), target=f.get_bld())

cp コマンドを実行するタスクの target 引数を間違えると依存関係がループしてしまいます。 get_bld() を使うことで、明示的にビルドディレクトリのパスを指定します。 source 引数は f でも良いのですが、対称性のために get_src() を明示的に呼び出しています。

実行してからビルドディレクトリの中身を表示させます。

$ waf distclean configure build
'distclean' finished successfully (0.002s)
Setting top to                           : /private/tmp/pep8-test
Setting out to                           : /private/tmp/pep8-test/_build
Checking for program pep8                : /private/tmp/pep8-test/bin/pep8
'configure' finished successfully (0.004s)
Waf: Entering directory `/private/tmp/pep8-test/_build'
[1/2] pep8 ${SRC}: pep8-test.py
[2/2] pep8-test.py: pep8-test.py -> _build/pep8-test.py
../pep8-test.py:2:3: E111 indentation is not a multiple of four
../pep8-test.py:4:1: E302 expected 2 blank lines, found 1
Waf: Leaving directory `/private/tmp/pep8-test/_build'
Build failed
 -> task failed (exit status 1):
        {task 4556299536: pep8 ${SRC} pep8-test.py -> }
' pep8 ../pep8-test.py '

$ ls _build
c4che/        config.log    pep8-test.py

どちらのタスクも実行されており、ビルドディレクトリに pep8-test.py がコピーされました。 しかし、 pep8 では構文エラーが報告されているのにコピーされてしまうのも困りものです。 それぞれのタスクが独立していると並列化させやすいですが、ここではそうではなく、 グループ化されていて欲しいところです。

タスクの順番を制御

Wafbook の 7.2. Build order constraints で3つの方法が説明されています。 タスクの set_run_after() メソッドを呼び出す方法、 タスク定義に before もしくは after 属性を持たせる方法、 そして、タスクの順番を自前で管理する方法です。 最後の方法は ジョブショップ・スケジューリング問題 (JSP) とも言われる問題領域ですから、 クリティカルパスがよほど単純な場合を除いては使わない方が良いと思います。

さて、タスク定義は waflib.Task.Task クラスを継承すれば良いので、 一番最初の方法 - set_run_after() - が簡単そうです。

スクリプト

cp 用のタスクと pep8 用のタスクを定義します。 それぞれのタスクに入出力パスを設定したら、順序関係を設定します。

from waflib.Task import Task

class cp(Task): 
    def run(self): 
        return self.exec_command('cp %s %s' % (
                self.inputs[0].abspath(), self.outputs[0].abspath()
            ))

class pep8(Task):
    def run(self):
        return self.exec_command('pep8 %s' % (self.inputs[0].abspath(),))

def build(bld):
    f = bld.path.find_resource('pep8-test.py')
    t1 = pep8(env=bld.env)
    t1.set_inputs(f)
    t2 = cp(env=bld.env)
    t2.set_inputs(f.get_src())
    t2.set_outputs(f.get_bld())
    t2.set_run_after(t1)
    bld.add_to_group(t1)
    bld.add_to_group(t2)

実行結果

実行してみると (pep8 でエラーの場合は) ビルドディレクトリに pep8-test.py がコピーされないことが分かります。

$ waf distclean configure build
'distclean' finished successfully (0.002s)
Setting top to                           : /private/tmp/pep8-test
Setting out to                           : /private/tmp/pep8-test/_build
Checking for program pep8                : /private/tmp/pep8-test/bin/pep8
'configure' finished successfully (0.004s)
Waf: Entering directory `/private/tmp/pep8-test/_build'
[1/2] pep8: pep8-test.py
/private/tmp/pep8-test/pep8-test.py:2:3: E111 indentation is not a multiple of four
/private/tmp/pep8-test/pep8-test.py:4:1: E302 expected 2 blank lines, found 1
Waf: Leaving directory `/private/tmp/pep8-test/_build'
Build failed
 -> task failed (exit status 1):
        {task 4367677712: pep8 pep8-test.py -> }
''

$ ls _build
c4che/      config.log

ソースコードを修正してから改めて実行してみると次のようになります。

$ waf distclean configure build
'distclean' finished successfully (0.002s)
Setting top to                           : /private/tmp/pep8-test
Setting out to                           : /private/tmp/pep8-test/_build
Checking for program pep8                : /private/tmp/pep8-test/bin/pep8
'configure' finished successfully (0.005s)
Waf: Entering directory `/private/tmp/pep8-test/_build'
[1/2] pep8: pep8-test.py
[2/2] cp: pep8-test.py -> _build/pep8-test.py
Waf: Leaving directory `/private/tmp/pep8-test/_build'
'build' finished successfully (0.169s)

$ ls _build
c4che/        config.log    pep8-test.py

先ほどの失敗した場合と比べて、2つのタスクの内2つとも実行されていることが分かります。

終わりに

Waf を使って pep8 で Python の構文をチェックしてからコピーする処理を記述しました。 同様にして nosetests などもフックに入れておくと、ソースコードを編集する度にテストを実行できます。 なお、まだ確認していませんが、テストはテスト用の枠組みでも実行できるはずです。

Waf 自体は Python で記述されていますが、そのターゲットの言語は選びません。 同じ方法は PHP にも適用可能で、 pep8 の代わりに php -l を使うことになるでしょう。 フロントエンドのリソース - JavaScript, CSS - に対しては YUI Compressor や Closure Tools でミニフィケーションできますね。

また、ここでは find_resource() を使ってノードを探し出しましたが、 ant_glob() を使うと Ant の探索パターンを活用できます。 プロジェクトのソースコードが増えてきた場合にはこちらを使ってループ処理を回した方が自然だと思います。

Makefile でも良いんだけど違う方法も覚えておきたい、 という場合には Waf も良さそうです。

蛇足ながら、ここではテキスト表記ですが、実際にコンソールで動かすと色が付きます。 意外とこれで導入障壁が下がったりするかもしれませんね。

コメントを投稿