自作コマンドでFlake8やmypyを一気に行う
はじめに
Flake8やmypyは、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
というファイルがあるはずなので、この絶対パスを書くと良い
-
- 無視するディレクトリ、規則等をリストで書いており、必要であればリストに追記する
#!/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を実行している
#!/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スクリプトである
#!/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スクリプトを作成する。
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を適用してみる。
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