2012年2月20日月曜日

自動テスト時にPEP8コーディング規約をチェックさせる

unittestを自動テストでまわす時に、PEP8をコーディング規約をチェックさせてます。
この手の規約を守るのは面倒くさいのですが、
Eclipse(PyDev)に特化させることで かなり効率が上がったので、紹介します。

まず、【ここ】を参考にして、PEP8チェックをunittestに組み込んでみました。

それだけだと芸が無いので、テスト結果の出力形式をPyDev用に編集しました。

PyDevは、トレースバックのログから、エラー箇所へのリンクを作りますよね?
これはログの出力結果のパターンから リンクを生成しているように思えたので、PEP8のログを整形してトレースバックログに似せてみました。
(この辺、Pythonは改造が簡単で良いですね)

すると、トレースバックのログと同様に、PEP8に違反したところへのリンクができました!
これで、PEP8違反箇所の修正が、かなり簡単になります。

以下、ソースです
(長いので、ダブルクリックしてコピー&ペーストしましょう)。
# -*- coding: utf-8 -*-

from os import path as os_path
import pep8
import unittest


#============================================================================
# BASE_DIR = PEP8チェック対象のルートディレクトリ

_SELF_DIR = os_path.dirname(os_path.abspath(__file__))
_PARENT_DIR = os_path.dirname(_SELF_DIR)

BASE_DIR = _PARENT_DIR

#============================================================================
# テスト実行時のパラメータ

NEW_LINE = '\n'
LINES = '-' * 50

DECODE_LIST = ['utf-8', 'mbcs']

ENCODE_FOR_ECLIPSE = 'utf-8'
ENCODE_FOR_CMD = 'mbcs'
DEF_ENCODE = ENCODE_FOR_ECLIPSE

#============================================================================
# PEP8実行時のパラメータのデフォルト定義

OUTPUT_STATICS = False

#EXCLUDE_PATTERNS = ['wtforms', 'dateutil']
EXCLUDE_PATTERNS = []
#IGNORE_PATTERNS = ['E501']
IGNORE_PATTERNS = []

ARGLIST = [
            '--statistics',
            '--filename=*.py',
            '--exclude=' + ','.join(EXCLUDE_PATTERNS),
            '--show-source',
            '--repeat',
            '--ignore=' + ','.join(IGNORE_PATTERNS),
            #'-v',
          ]

#============================================================================


class StoreMessages(object):
    u"""pep8.message の出力タイミングを遅らせるためのクラスです

    以下のように使うことで、test実行中にpep8からprintされてしまうtextを
    self.textに溜め込みます。
    >>> sm = StoreMessage()
    >>> pep8.message = sm.message
    こうすることで、assertError時のメッセージに、pep8エラー内容を含めることを
    目的にしています。
    """

    def __init__(self, decode_list=DECODE_LIST):
        self.text = u''
        self.decode_list = decode_list

    def message(self, text=u''):
        for code in self.decode_list:
            try:
                self.text += text.decode(code) + NEW_LINE
                break
            except UnicodeDecodeError:
                pass
        else:
            self.text += text.decode(code) + NEW_LINE

    def get_message(self, code=None):
        _code = code or DEF_ENCODE
        return self.text.encode(_code)

#============================================================================


def report_error(self, line_number, offset, text, check):
    u""" pep8.Checker.report_errorの代替の関数です。ほぼ完コピです。

    以下のように使うことで、pep8で出力するエラー内容を整形します。
    >>> pep8.Checker.report_error = report_error
    これにより、tracebackのメッセージと同じ形になるので、
    eclipse上でpep8のエラーをクリックできるようにします。
    """

    code = text[:4]
    if pep8.ignore_code(code):
        return
    if pep8.options.quiet == 1 and not self.file_errors:
        pep8.message(self.filename)
    if code in pep8.options.counters:
        pep8.options.counters[code] += 1
    else:
        pep8.options.counters[code] = 1
        pep8.options.messages[code] = text[5:]
    if pep8.options.quiet or code in self.expected:
        # Don't care about expected errors or warnings
        return
    self.file_errors += 1
    if pep8.options.counters[code] == 1 or pep8.options.repeat:
        pep8.message(LINES)
        pep8.message(('  File "%s", line %s, column %d' + NEW_LINE +
                      '    %s:') % (self.filename,
                                   self.line_offset + line_number,
                                   offset + 1, text))
        if pep8.options.show_source:
            line = self.lines[line_number - 1]
            pep8.message(line.rstrip())
            pep8.message(' ' * offset + '^')
        if pep8.options.show_pep8:
            pep8.message(check.__doc__.lstrip('\n').rstrip())


def do_pep8(base_dir, opt_list=ARGLIST, output_statics=OUTPUT_STATICS):
    u""""base_dir以下のファイルに対して、pep8のチェックを行います"""

    store = StoreMessages()
    pep8_message = pep8.message
    pep8.message = store.message
    pep8_report_error = pep8.Checker.report_error
    pep8.Checker.report_error = report_error

    arglist = opt_list[:]
    arglist.append(base_dir)

    options, args = pep8.process_options(arglist)
    runner = pep8.input_file

    for path in args:
        if os_path.isdir(path):
            pep8.input_dir(path, runner=runner)
        elif not pep8.excluded(path):
            options.counters['files'] += 1
            runner(path)

    if output_statics:
        store.message(LINES)
        for statics_mes in pep8.get_statistics():
            store.message(statics_mes)

    store.message(LINES)

    pep8.message = pep8_message
    pep8.Checker.report_error = pep8_report_error

    errors = pep8.get_count('E')
    warnings = pep8.get_count('W')
    message = 'pep8: %d errors / %d warnings' % (errors, warnings)
    message += NEW_LINE + store.get_message()

    return errors, warnings, message


class Test_pep8(unittest.TestCase):

    def test_pep8(self):

        errors, warnings, message = do_pep8(BASE_DIR)
        self.assertEqual(errors + warnings, 0, message)


def main():
    print u'対象フォルダ:', BASE_DIR
    print u'パラメータ:', ARGLIST
    DEF_ENCODE = ENCODE_FOR_CMD
    unittest.main()


if __name__ == '__main__':
    main()

# EOF

<<設定について>>

BASE_DIR:ここで指定されたパスを起点にPEP8のチェックをします。

このソース例だと、このモジュールのおいてあるフォルダの上の階層を指定していますが、
これは、TESTケースは1つのパッケージに集めると想定だからです。
(特に、GoogleAppEngineの開発では、本番に単体テストのコードをアップロードさせないようにする為にも、一つのパッケージに集まると思います)


EXCLUDE_PATTERNS = PEP8のチェックから外したいディレクトリをリストで指定します。

GAEだと、wtformsとかの外部のライブラリをソースディレクトリに含める必要があります。規約は無視しているライブラリを、PEP8チェックから外すためのパラメータです。


IGNORE_PATTERNS = PEP8のチェック対象外としてい項目をしていします。

イッシーは、E501(1行の文字数制限)とか、嫌いなので外してたりします。


ENCODE_FOR_ECLIPSE = 外部のテストランナから呼ばれた時の出力文字コード
ENCODE_FOR_CMD = コマンドプロンプトから実行した時の出力文字コード

Windowsのコマンドプロンプトの文字コードは、mbcsなのですが、
PyDevのプロンプトはutf-8なので、分けてます。

Windows + PyDevじゃない人は、ほかの設定のほうがよいかも。


DECODE_LIST = PEP8が出力してくるログをデコードする際の文字コードのリスト

リスト順にデコードします。



<<ソースの適当解説>>

49~117行目は、pep8の出力を整形するためのコードです。
123~127行目で、既存のpep8のコードを整形するためのコードで上書きしてます。
そのままだと気持ちが悪いので、149~150行目で、元に戻しています。

でも、なんかもっと良い書き方がありそうですねー。ご意見募集中です。

0 件のコメント:

コメントを投稿