Python: pre-commitでコミット前にチェックする

Pythonで安全にコーディングしようとすると、リンタ (ex. flake8) やフォーマッタ (ex. black) 、型チェッカ (ex. mypy) など、コミット前に実行するコマンドが増えていきます。

今回は、コミット時にコマンドを自動的にフックするPythonのツールとしてpre-commitを紹介します。

インストール

pre-commitはpipでインストールできます。

$ python --version
Python 3.8.5

$ pip install pre-commit

$ pre-commit --version
pre-commit 2.7.1

$ pre-commit help
usage: pre-commit [-h] [-V] {autoupdate,clean,hook-impl,gc,init-templatedir,install,install-hooks,migrate-config,run,sample-config,try-repo,uninstall,help} ...
...

設定

次に設定ファイルを追加します。sample-configサブコマンドで生成できます。

  • ファイル名は .pre-commit-config.yaml にする必要があります
$ pre-commit sample-config > .pre-commit-config.yaml

.pre-commit-config.yamlは以下のような内容となってます。
処理を記述したPythonファイルがリモートレポジトリ (repo) にアップロードされており、コミット時には hooks 以下に id で列挙された処理を順に実行する、という設定となります。

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.4.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files

https://github.com/pre-commit/pre-commit-hooks 以外にも、ツールの製作者がpre-commit用のフックを用意していることもあります。例えばblackを実行する場合は以下のように記述します (blackのREADMEを参照) 。

  • フックはレポジトリ直下の .pre-commit-hooks.yaml に定義されています (idの値もこのファイルで確認できます)
  • 主要なフックのリストは https://pre-commit.com/hooks.html で列挙されています
  • もちろん自分で作ることもできます (参考)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
...
- repo: https://github.com/psf/black
  rev: 19.10b0
  hooks:
    - id: black
      language_version: python3

フックの実行

サンプルとして、flake8とblackをフックに登録した設定ファイルを用意します。

- repo: https://github.com/psf/black
  rev: 19.10b0
  hooks:
    - id: black
      language_version: python3
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.4.0
    hooks:
    -   id: flake8

pre-commitでフックする場合、設定ファイルを作成後にinstallを実行します。これでスクリプトが生成されていますので、設定ファイルの変更都度実行する必要があります。

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

$ cat .git/hooks/pre-commit
#!/usr/bin/env python3.8
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

# ... 省略 ...

if sys.platform == 'win32':  # https://bugs.python.org/issue19124
    import subprocess

    if sys.version_info < (3, 7):  # https://bugs.python.org/issue25942
        raise SystemExit(subprocess.Popen(CMD).wait())
    else:
        raise SystemExit(subprocess.call(CMD))
else:
    os.execvp(CMD[0], CMD)

試しに以下のmain.pyファイルをコミットしてみます。

def print_hi(name, age):
    profile = f'{name} ({str(age)})'
    print(f'Hi, {name}')

if __name__ == '__main__':
    print_hi('ohke', 32)

これでコミットしてみますが、エラーとなって失敗します。flake8ではprofileを使っていないことを指摘され、blackではリフォーマットされていることがわかります。

  • コードを変更したファイルのみがチェックの対象となります
$ git commit -m "test"
Flake8...................................................................Failed
- hook id: flake8
- exit code: 1

main.py:2:5: F841 local variable 'profile' is assigned to but never used

black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted main.py
All done! ✨ 🍰 ✨
1 file reformatted.

コードを修正して改めてコミットすると、無事にflake8とblackにパスして、コミットに成功します。

$ git commit -m "test"
Flake8...................................................................Passed
black....................................................................Passed
[master (root-commit) 9ec7bc8] test
 1 file changed, 7 insertions(+)
 create mode 100644 main.py

まとめ

今回はpre-commitについて紹介しました。

pre-commit自体はPythonのツールですが、フックは他の言語でも実装可能ですので、適用範囲は広いツールかと思います。