12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Style guide に即していない Python コードを自動で弾く

Last updated at Posted at 2018-03-08

問題点として

Style guide に即していないコードのプルリクエストが来た場合、レビューでは Style guide の確認は困難です。

解決する手段として

Style guide をチェックするテストを組込み、Github と Circle CI の integration により、commit や PR の際に自動で Style guide をチェックできるようにします。

使用するツール、サービス

次のツール、サービスを使用します。

  • nose による単体テスト
  • PyLintによるのStyle guideのチェック
  • Githubによるバージョン管理
  • Circle CIによる継続的インテグレーション

※Python3.6を使用します。

プロジェクトの構成

ここでは架空のPythonモジュール "my_tools" を開発している仮のプロジェクトを使用します。
最初のプロジェクトの構成です。

.
|____my_tools
| |______init__.py
| |_____adder.py
|____requirements.txt

モジュール my_tools には、加算器adder.pyが入っています。

my_tools/adder.py
# -*- coding: utf-8 -*-
class Adder:
    def __init__(self) -> None:
        pass

    def add(self, x: int, y: int) -> int:
        return x + y

requirements.txt は本記事中で依存するを記述しておきます。

requirements.txt
nose
pylint

このプロジェクトにnoseによるテスト、PyLintによるコーディングスタイルのチェック、Circle CIによる継続的インテグレーションの仕組みを加えていきます。

nose

noseによるテストを加えます。

.
|____my_tools
| |______init__.py
| |_____adder.py
|____tests
| |____my_tools_tests
|   |____test_adder.py

test_adder.py にテストを記述します。

tests/my_tools_tests/test_adder.py
# -*- coding: utf-8 -*-
"""Adder のユニットテストが格納されたモジュール."""

from unittest import TestCase
from nose.tools import eq_
from my_tools.adder import Adder


class AdderTestCase(TestCase):
    """Adder クラスのユニットテスト."""

    def setUp(self) -> None:
        """各テスト毎の初期化."""
        pass

    def tearDown(self) -> None:
        """各テスト毎の後処理."""
        pass

    def test_adder(self) -> None:
        """加算メソッドのテスト."""
        adder = Adder()
        eq_(3, adder.add(1, 2))

テストを実行してみましょう。

$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.009s

OK

テストが通りました。
noseによるテストの詳細に関してはここでは省きます。

PyLint

PyLintは論理エラーのチェックとPEP8スタイルガイドのチェックを行うツールです。

コマンドラインからの実行

モジュール my_tools に PyLint をかけてみましょう。

$ pylint my_tools/
No config file found, using default configuration
************* Module my_tools.adder
C:  1, 0: Missing module docstring (missing-docstring)
C:  2, 0: Missing class docstring (missing-docstring)
C:  6, 4: Argument name "x" doesn't conform to snake_case naming style (invalid-name)
C:  6, 4: Argument name "y" doesn't conform to snake_case naming style (invalid-name)
C:  6, 4: Missing method docstring (missing-docstring)
R:  6, 4: Method could be a function (no-self-use)
R:  2, 0: Too few public methods (1/2) (too-few-public-methods)

-------------------------------------------------------------------
Your code has been rated at -4.00/10

いくつかメッセージが出力されましたね。
メッセージの先頭の記号はメッセージタイプと 行番号、列番号を示しています。

  • [R]efactor for a “good practice” metric violation
  • [C]onvention for coding standard violation
  • [W]arning for stylistic problems, or minor programming issues
  • [E]rror for important programming issues (i.e. most probably bug)
  • [F]atal for errors which prevented further processing

エラーと警告を全て消えるように対処します。
そのための方策として下記2点を行います。

  • ソースコードを修正する。
  • PyLint 側を設定する。

以降でこれらのメッセージがなくなるよう対応していきます。

ソースコードの修正

adder.pyとdivider.pyを下記のように修正しました。

adder.py
# -*- coding: utf-8 -*-
"""加算に関するユーティリティーが入っているモジュールです."""


class Adder:
    """加算器."""

    def __init__(self) -> None:
        """初期化."""
        self.result = 0

    def add(self, x: int, y: int) -> int:
        """加算を実行するメソッド.

        :param x: 整数x
        :param y: 整数y
        :return: x と y の演算結果
        """
        self.result = x + y
        return self.result

コードチェックの結果を確認してみましょう。

$ pylint my_tools/
No config file found, using default configuration
************* Module my_tools.adder
C: 12, 4: Argument name "x" doesn't conform to snake_case naming style (invalid-name)
C: 12, 4: Argument name "y" doesn't conform to snake_case naming style (invalid-name)
R:  5, 0: Too few public methods (1/2) (too-few-public-methods)

-------------------------------------------------------------------
Your code has been rated at 5.00/10 (previous run: -4.00/10, +9.00)

いくつか減りましたね。
残りは次で対応します。

エラーと警告の抑制する

先のコードチェック結果では引数x,yはスネークケースではないとの指摘がなされていました。
今度はソースコードを修正せず、チェックの抑制による解決を試みます。

実際の開発においても、過去に実績のあるコードなどで、そのコードに手を加えることが現実的でない場合もあります。
このような場合にも有効な手段になります。
設定には 3通りのやり方 があります。

  • コマンドラインオプションでの設定
$ pylint my_tools/ --disable='invalid-name,too-few-public-methods'
No config file found, using default configuration

-------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 5.00/10, +5.00)

--disable (-d) オプションで invalid-name と too-few-public-methods のチェックを抑制することができました。

  • ソースコード上での設定

ファイルの冒頭でコメントの形式で下記コードを追加してみましょう。

adder.py
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name,too-few-public-methods
"""加算に関するユーティリティーが入っているモジュールです."""

なお、クラスやメソッドの前に記述することにより細かい制御もできます。

$ pylint my_tools/
No config file found, using default configuration

--------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

エラーは抑制されました。

  • 設定ファイルを使用した設定

設定ファイルを生成します。

$ pylint --generate-rcfile > .pylintrc

.pylintcの[MESSAGES CONTROL]にあるdisable= に今回の invalid-name,too-few-public-methods を追加します。

PyLint によるチェックを実行します。

$ pylint my_tools/
Using config file ....../test_proj_pylint/.pylintrc

--------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

同様にメッセージは出力されなくなりました。

PyLintをnoseテストから実行する

これらのコードチェックをユニットテストの1つとして実行できるようにします。
これは Jenkins や Circle CI のような継続的インテグレーションでコードチェックも行うための工夫です。
なおテストには nose を使用しますが、どのテスティングフレームワークでも要点は同じです。

テストのディレクトリに my_tools_tests/test_pylint.py を加えます。

|____tests
| |____my_tools_tests
| | |____test_pylint.py   ← 追加
| | |____test_adder.py

test_pylint.py の内容は次のようになります。

test_pylint.py
# -*- coding: utf-8 -*-
"""PyLint のコードチェックを行うテストが格納されたモジュール."""

from subprocess import PIPE, Popen
from unittest import TestCase

from nose.tools import ok_
from pylint import epylint as lint


class PyLintTestCase(TestCase):
    """PyLintによるコードチェックを行うためのクラス."""

    def setUp(self) -> None:
        """Run setup function."""
        pass

    def tearDown(self) -> None:
        """Run tear down function."""
        pass

    def test_pylint_conformance(self) -> None:
        """PyLintによるコードチェックを行うテスト."""
        # 検査対象とするディレクトリ名
        dir_name = "my_tools"
        # 検査から除外するエラーや警告
        disables = [
            "R0801",    # Similar lines in 2 files.
            "C0326",    # bad-whitespace
            "C0103",    # invalid-name
            "R0903",    # too-few-public-methods
        ]
        # 検査から除外するファイル
        ignore_files = [
            "__init__.py",
        ]
        # PyLintに渡すオプション
        options = [
            "--max-line-length=160",
            "--variable-rgx='[a-z_]([a-z0-9_])?'",  # 変数名: v, v1, val_hoge1, __val 等
            f"--disable={','.join(disables)}",
            f"--ignore-patterns={','.join(ignore_files)}",
        ]
        cmd = " ".join([dir_name, " ".join(options)])

        # PyLint を実行します.
        # 結果は pylint_stdout に格納されます.
        pylint_stdout, _ = lint.py_run(cmd, return_std=True)

        # pylint_stdout から区切りの行と前後の空白、改行を取り除いた文字列を
        # messages に格納します.
        messages = []
        for line in pylint_stdout.readlines():
            s = line.strip()
            if len(s) and not s.startswith(("*", "-")):
                messages.append(s)

        if len(messages) > 1:   # 最初の1行目は結果にかかわらず表示されるサマリー.
            # テスト失敗
            # messages に何か内容が入っていればテストは失敗とみなし.内容をコンソールに表示する.
            ok_(False, "\n".join(messages))
        else:
            # テスト成功
            # messages に警告やエラーが無ければテストは成功とみなす.
            ok_(True)

PyLint を Python スクリプトから実行する箇所は中央部の下記コードになります。

test_pylint.py
pylint_stdout, _ = lint.py_run(cmd, return_std=True)

PyLintの設定をこのメソッドの前半部分で作成しています。

  • チェック対象とするディレクトリ
  • 抑制するエラーや警告
  • チェック対象外とするファイル
  • オプション
        # 検査対象とするディレクトリ名
        dir_name = "my_tools"
        # 検査から除外するエラーや警告
        disables = [
            "R0801",    # Similar lines in 2 files.
            "C0326",    # bad-whitespace
            "C0103",    # invalid-name
            "R0903",    # too-few-public-methods
        ]
        # 検査から除外するファイル
        ignore_files = [
            "__init__.py",
        ]
        # PyLintに渡すオプション
        options = [
            "--max-line-length=160",
            "--variable-rgx='[a-z_]([a-z0-9_])?'",  # 変数名: v, v1, val_hoge1, __val 等
            f"--disable={','.join(disables)}",
            f"--ignore-patterns={','.join(ignore_files)}",
        ]
        cmd = " ".join([dir_name, " ".join(options)])

その他の番号と名称の対応は下記公式ドキュメント の Pylint checkers' options and switches を参照してください。


メソッドの後半部分でチェック結果を判定しています。

  • 装飾行を取り除き、メッセージの有無を判定します。
  • メッセージが蓄積されていれば、テストは失敗とし、内容をコンソールに表示させます。
  • 判定方法には他にもrated at 10.00/10 を検知できたら成功とみなすなど、別の手段も考えられます。
        # pylint_stdout から区切りの行と前後の空白、改行を取り除いた文字列を
        # messages に格納します.
        messages = []
        for line in pylint_stdout.readlines():
            s = line.strip()
            if len(s) and not s.startswith(("*", "-")):
                messages.append(s)

        if len(messages) > 1:   # 最初の1行目は結果にかかわらず表示されるサマリー.
            # テスト失敗
            # messages に何か内容が入っていればテストは失敗とみなし.内容をコンソールに表示する.
            ok_(False, "\n".join(messages))
        else:
            # テスト成功
            # messages に警告やエラーが無ければテストは成功とみなす.
            ok_(True)

テストを実行してみましょう。

$ nosetests
..
----------------------------------------------------------------------
Ran 2 tests in 1.268s

OK

テストはOKでした。

Circle CI

ここではプロジェクトをGithubにプッシュすることでCircle CIによる継続的インテグレーションが実行される手順を記します。

GithubとCircle CI

事前にGithubのレポジトリとCircle CIを関連付けておきます。

設定ファイル

Circle CIのためを設定ファイルを作成します。

.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: python:3

    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "57:f1:**:**:**:**:**:**:**:**:**:**:**:**:**:**"
            - "00:5e:**:**:**:**:**:**:**:**:**:**:**:**:**:**"
      - run:
          name: Install dependencies
          command: pip install -r requirements.txt
      - run:
          name: Run tests
          command: python setup.py nosetests

workflows:
  version: 2

  build_deploy:
    jobs:
      - build

設定内容について

workflows

Circle CI 2.0ではworkflowsにjobsを追加していくことで処理を記述していきます。

.circleci/config.yml
workflows:
  version: 2

  build_deploy:
    jobs:
      - build

ここではbuildというjobが実行されます。

jobs

各jobはファイル上部に記載されています。
jobは使用するVMイメージと各処理を実行するstepsを記述します。

.circleci/config.yml
jobs:
  build:
    docker:
      - image: python:3

    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "57:f1:**:**:**:**:**:**:**:**:**:**:**:**:**:**"
            - "00:5e:**:**:**:**:**:**:**:**:**:**:**:**:**:**"
      - run:
          name: Install dependencies
          command: pip install -r requirements.txt
      - run:
          name: Run tests
          command: python setup.py nosetests
  • checkout: Githubからコードを取得します。
  • add_ssh_keys: GithubとCircle CIを関連付けるフィンガープリントを記述します。
  • run: noseのテストを実行します。

Circle CIによるテストの実行

この設定でGithubにプッシュすると、noseのテストが実行されます。
noseのテスト内でPyLintによるスタイルガイドのチェックも同時に行われます。

以下はCircle CIでスタイルガイドのチェックを行った際のスクリーンショットです。
スタイルガイドのチェックに引っかかったブランチは Circle CI で失敗とみなされます。
(赤い表示の部分)

shot1_edited.png

結果として

Style guide のチェックが commit ごとに行われるようになります。開発者は PR 前に Style guide に即していないコードを他の単体テストと同様に予め確認できるようになります。

これにより、レビュー者は PR によるレビューを依頼されたコードに関しては Style guide に即しているという前提でレビューを進めることができるようになります。

サンプルスクリプト

GitHubのこちらのリポジトリに公開しています。

参考元

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?