🔨

自作コマンドでFlake8やmypyを一気に行う

2022/11/09に公開

はじめに

Flake8mypyは、Pythonのコードを静的解析するツールである。Flake8はPEP8のコーディング規約、mypyは型ヒントのミスをチェックする。

ただし、全ての規約に従ってコーディングするのはナンセンスなケースもあり得る。この場合、コマンドを追記することで、一部の規約を無視できる。

flake8 [.pyファイル/ディレクトリのパス] --ignore=E501
mypy [.pyファイルの/ディレクトリのパス]  --ignore-missing-imports

PEP8のE501は1行の文字数を79文字以下に制限する規則であり、一般的には不要だろう。
mypyの--ignore-missing-importsは、3rd-partyモジュールをimportしたときに発生するエラーで、これも無視したい。

上記のように、コマンド引数を追記すればいいのだが、これは利便性に欠けていると思う。
なぜなら、引数の暗記や、長文のコマンド入力が必要だからである。

そこで、自作コマンドcheck_pycodeを作成して、Flake8とmypyのコードチェックを一発で行う方法を解説する。

実行環境

  • Ubuntu22.04
  • Python3.11.0

ファイル構成

.
├── venv                 # Pythonの仮想環境
│   ├──...
|── pysrc
│   ├── check_pycode.py  # flake8とmypyを実行するPythonスクリプト
│   └── settings.py      # 設定ファイル
└── check_pycode.sh      # check_pycodeを実行するshellスクリプト

コマンドcheck_pycodeの作成

下記のプロセスでコマンドを作成する。

  • 仮想環境の作成
  • スクリプトの作成
  • シンボリックの作成(shellスクリプトのコマンド化)

以降、順次説明する。

仮想環境の作成

Pythonの仮想環境を作成して、そこにFlake8とmypyを入れる。

cd [ワークディレクトリのパス]
python -m venv venv
venv/bin/python -m pip install flake8 mypy

この時点で、ワークディレクトリにvenvフォルダが生成されている。
ここにPythonの仮想環境が入っていて、Flake8とmypyがインストールされている。

スクリプトの作成

まず、各ディレクトリとファイルを作成する。

mkdir pysrc
touch pysrc/check_pycode.py
touch pysrc/settings.py
touch check_pycode.sh

各ファイルの中身は、下記の通り。

pysrc/settings.py
  • PYTHON_PATHには、仮想環境のパスを書く
    • venv/bin/pythonというファイルがあるはずなので、この絶対パスを書くと良い
  • 無視するディレクトリ、規則等をリストで書いており、必要であればリストに追記する
pysrc/settings.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

# ********************
# 共通のパラメータ
# ********************
PYTHON_PATH = ""  # 仮想環境のパスを書く
# 探索を行わないディレクトリやファイルのパターン
EXCLUDE_PATTERN_LIST = [
    ".git",
    "__pycache__",
    "venv",
]


# ********************
# Flake8のパラメータ
# ********************
# 無視するルール
# ルールの名称と内容は、下記URLを参考にする
#   * https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
#   * https://flake8.pycqa.org/en/latest/user/error-codes.html
# 探索を無視するフォルダ名のリスト
FLAKE8_EXCLUDE_RULE_LIST = [
    "E501",  # line too long
]


# ********************
# mypyのパラメータ
# ********************
# mypyに追加する引数
MYPY_ARGS_LIST = [
    "--ignore-missing-imports",  # missing-importを無視
]


pysrc/check_pycode.py
  • subprocessでFlake8とmypyを実行している
pysrc/check_pycode.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import argparse
from pathlib import Path
import subprocess

import settings


def exec_cmd(cmd):
    print("*" * 40)
    print(f"Execute: {cmd}")
    proc = subprocess.Popen(cmd, shell=True, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = proc.communicate()
    for line in stdout.splitlines():
        print(f"[stdout]: {line}")
    for line in stderr.splitlines():
        print(f"[stderr]: {stderr}")


def check_by_flake8(pyfile: Path):
    cmd_list = [
        f"{settings.PYTHON_PATH} -m",
        f"flake8 {pyfile}",
    ]
    if settings.EXCLUDE_PATTERN_LIST:
        ignore_patterns = ",".join(settings.EXCLUDE_PATTERN_LIST)
        cmd_list += [f" --exclude={ignore_patterns}"]
    if settings.FLAKE8_EXCLUDE_RULE_LIST:
        ignore_rules = ",".join(settings.FLAKE8_EXCLUDE_RULE_LIST)
        cmd_list += [f" --extend-ignore={ignore_rules}"]

    cmd = " ".join(cmd_list)
    exec_cmd(cmd)


def check_by_mypy(pyfile: Path):
    cmd_list = [
        f"{settings.PYTHON_PATH} -m",
        f"mypy {pyfile}",
    ]
    for ignore_pattern in settings.EXCLUDE_PATTERN_LIST:
        cmd_list += [f" --exclude {ignore_pattern}"]
    for args in settings.MYPY_ARGS_LIST:
        cmd_list += [f" {args}"]

    cmd = " ".join(cmd_list)
    exec_cmd(cmd)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("pypath")
    args = parser.parse_args()

    # ****************
    # define variables
    # ****************
    # variables from ArgmentParser
    pypath = Path(args.pypath)

    # ****************
    # main process
    # ****************
    check_by_flake8(pypath)
    check_by_mypy(pypath)


if __name__ == "__main__":
    main()

check_pycode.sh
  • pysrc/check_pycode.pyを実行するshellスクリプトである
check_pycode.sh
#!/bin/sh

# ****************
# define variables
# ****************
BASE_DIR=$(cd $(dirname $(readlink $0)); pwd)
CMD_NAME="check_pycode"
PYSRC_PATH="${BASE_DIR}/pysrc/${CMD_NAME}.py"

# ****************
# main process
# ****************
python $PYSRC_PATH $1

シンボリックリンクの作成

この時点で、check_pycode.sh [pythonのパス]を実行すれば、flake8とmypyが走るようになっている。
これをコマンドとして使えるようにするため、シンボリックリンクを作成する。

ln -si [check_pycode.shの絶対パス] /usr/local/bin/check_pycode

以上で、自作コマンドcheck_pycodeの作成は完了である。

使用例

例として、PEP8の規約違反や、型ヒントのミスを含むpythonスクリプトを作成する。

test_mistake.py
import numpy as np

a: str = 100
b= np.zeros(2) + a

print(b)
print("very very very very very very very very very very very very very very very long line")


このスクリプトに対して、check_pycodeを実行してみる。

check_pycode test_mistake.py

結果

****************************************
Execute: [仮想環境のパス] -m flake8 test_mistake.py  --exclude=.git,__pycache__,venv  --extend-ignore=E501
[stdout]: test_mistake.py:4:2: E225 missing whitespace around operator
[stdout]: test_mistake.py:8:1: W391 blank line at end of file
****************************************
Execute: [仮想環境のパス] -m mypy test_mistake.py  --exclude .git  --exclude __pycache__  --exclude venv  --ignore-missing-imports
[stdout]: test_mistake.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str")  [assignment]
[stdout]: Found 1 error in 1 file (checked 1 source file)

subprocessで実行しているコマンドと、その標準出力、標準エラーを出力している。
実行結果より、flake8に関してはE225とW391、mypyに関してはstrとintのミスを検知できている。
また、flake8の規約E501や、mypyのimport-missingのエラーが無視されているのも確認できる。(settings.pyで設定したため。)

上記のコードを修正したものが下記で、これにもcheck_pycodeを適用してみる。

test_correct.py
import numpy as np

a = 100
b = np.zeros(2) + a

print(b)
print("very very very very very very very very very very very very long line")

check_pycode test_correct.pyの実行結果は下記の通り。

****************************************
Execute: [仮想環境のパス] -m flake8 test_correct.py  --exclude=.git,__pycache__,venv  --extend-ignore=E501
****************************************
Execute: [仮想環境のパス] -m mypy test_correct.py  --exclude .git  --exclude __pycache__  --exclude venv  --ignore-missing-imports
[stdout]: Success: no issues found in 1 source file

flake8とmypyのどちらのエラーも出ていないことが確認できる。

おわりに

今回、自作コマンドを作成して、flake8とmypyのチェックを一気に実行する方法について解説した。
コーディングスタイルは十人十色だが、「無視する規約の追加」「subprocessで叩くコマンドの引数追加」等のカスタマイズも容易なため、使いやすいツールだと思う。

Discussion