pre-commitでコミット時にコードの整形やチェックを行う
この記事は?
pre-commit
というフレームワークを用いて,コミット時にコードの整形や型チェック,コードの複雑度の計算などを行えるようにしました.その際に自分が調べた内容をこちらに書いておきます.言語はpythonを想定していますが,pre-commit
では他の言語のhookスクリプトも使用することができます.至らぬ点が多々あると思いますが,コメントにてご指摘いただけると幸いです.
この記事でわかること
-
pre-commit
のインストール / 使い方 - GitHubで公開されているhookの紹介
pre-commit-hooks
isort
black
mypy
markdownlint
flake8
-
flake8
の拡張機能の紹介flake8-bugbear
flake8-builtins
flake8-eradicate
pep8-naming
flake8-expression-complexity
flake8-cognitive-complexity
背景
pythonのコードを書く際に,blackやisortを用いてソースコードをフォーマットしたり,flake8やmypyでコードをチェックしている方も多いと思います.
自分はVSCodeを使っていて,自動でフォーマットするような設定にしていましたが,ある日を境にisortがうまく動かなくなってしまいました(参考).
isortを逐一実行するのは面倒なので,commit時に勝手に実行されてくれたら良いなと思い,調べてみたところpre-commit
というパッケージを発見しました.
pre-commit
とは
Gitのpre-commit hookを簡単に管理できるフレームワークです.複数の言語のhookに対応しています.
インストール
conda
, pip
でインストールできます.Macを使用している場合はbrew
でもインストール可能です.
$ conda install -c conda-forge pre-commit
$ pip install pre-commit
$ brew install pre-commit
インストールが完了したら,以下のコマンドが実行されることを確認してください.
$ pre-commit --version
pre-commit 2.7.1
使い方
ターミナルにて,pre-commit hookを適用したいGit管理のディレクトリに移動したら,以下のコマンドを実行してください.
$ pre-commit sample-config > .pre-commit-config.yaml
このコマンドによって,pre-commitの設定ファイルを作成することができます.
設定ファイルの名前は.pre-commit-config.yaml
となるようにします.
中身を見てみると以下のようになっているはずです.
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
repos
repo
, rev
, hooks
の3つを記したrepositoy mappingのリストになっています.
repo
hookのためのコードが,どのGitHubレポジトリからのものかを記載します.
rev
GitHubのレポジトリのブランチ名,もしくはリリースのバージョンかタグを指定します.
hooks
どのhookを使うか(hook mapping)のリストを記したものです.
hook mappingは,どのhookかを示すid
とその他のオプションのパラメータで記述します.
その他のパラメータについては,公式ドキュメントを参照してください.
実際にpre-commitを使用してみる
Gitのhookスクリプトをセットアップするために以下のコマンドを実行します.
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
インストールが完了したら,コミットしてみます.
$ git add .pre-commit-config.yaml
$ git commit -m "pre-commit"
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
こんな感じでhookスクリプトが実行されたのが確認できたと思います.
一度セットアップが完了したディレクトリでは,今後コミットの前にpre-commit install
を実行する必要はありません.
今回はhookが全てpassしていたのでそのままコミットができましたが,hookが失敗した場合には該当箇所の修正(フォーマッタに関するhookは自動で修正してくれるので手動での修正は不要)を行ってから,git add
→ git commit
を実行する必要があります,
GitHubで公開されているhookの紹介
GitHubでは様々なhookが公開されています.
使用可能なhookの一覧は,こちらを参照してください.
今回は個人的に使えると思ったhookの紹介と,それらを使用できるように設定を変更していきます.
なおlocal hookに関しては,公式ドキュメント,もしくはPython製のツールpre-commitでGitのpre-commit hookを楽々管理!!を参照してください.
pre-commit-hooks
sample-config
にも使われていたpre-commit-hooks
では,様々なhookが提供されています.
-
check-added-large-files
容量が大きいファイルがコミットされるのを防ぐ -
check-json
jsonファイルのsyntaxをチェック -
check-toml
TOMLファイルのsyntaxをチェック -
check-xml
xmlファイルのsyntaxをチェック -
check-yaml
yamlファイルのsyntaxをチェック -
debug-statements
debuggerのインポートやpython3.7以上で用いられるbreakpoint()
があるかどうかを確認する -
detect-aws-credentials
AWSのcredentialファイルがあるかどうかを確認.--allow-missing-credentials
オプションを追加することで,credentailファイルがない場合でもhookが通るようになる -
detect-private-key
秘密鍵があるかどうかをチェックする -
end-of-file-fixer
ファイルの最後が改行で終わっているかどうかを確認する -
no-commit-to-branch
特定のブランチに直接コミットできないようにする.args: [--branch, branch_name]
を指定することで,どのブランチにコミットできないようにするかを設定できる.デフォルトでは,master
ブランチに直接コミットができないようになる. -
pretty-format-json
jsonファイルの構文チェックとフォーマットが行える.--autofix
オプションを追加することで,自動でフォーマットしてくれる.
以上のhookを追加した設定が以下です.
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: check-added-large-files
- id: check-json
- id: check-toml
- id: check-xml
- id: check-yaml
- id: debug-statements
- id: detect-aws-credentials
args: [--allow-missing-credentials]
- id: detect-private-key
- id: end-of-file-fixer
- id: no-commit-to-branch # to protect specific branches from direct checkins.
args: [--branch, master]
- id: pretty-format-json
args: [--autofix]
isort
import文を並び替えるためのパッケージです.今回はblack
というフォーマッターと併用するのですが,isort
とblack
のimport文の整形は,デフォルトの設定のままだと競合してしまいます.これは--profile black
という引数を追加することで解決できます.(参考)
- repo: https://github.com/pycqa/isort
rev: 5.5.2
hooks:
- id: isort
args: ["--profile", "black"]
black
pythonのフォーマッターです.他のフォーマッターよりも制限が厳しく,整形後はほとんどコードが一意に決まるのが特徴です.詳しくは,もうPythonの細かい書き方で議論しない。blackで自動フォーマットしようが参考になります.
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
language_version: python3
mypy
pythonではバージョン3.5から型アノテーションという機能が追加されました.この型アノテーションは,通常実行時にはチェックされないのですが,mypy
というライブラリを使うことで,型アノテーションを元に静的解析を行うことができます.今回はpre-commit
のためにミラーしたレポジトリを使用します.mypy
のオプションに関しては,mypyのコマンドラインオプションたち
を参考にしてください.
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.790
hooks:
- id: mypy
args: [--ignore-missing-imports]
flake8
flake8
はpythonのコードのチェックツールです.コードのエラーのチェックや循環的複雑度の測定などをしてくれます.また拡張機能を追加することで,より詳しいコードの解析が行ったり,認知的複雑度や命名に関する警告など,様々なことができるようになります.flake8
の拡張機能の詳しい紹介については後述します.pre-hook
で拡張機能を使用するには,additional_dependencies
にパッケージの名前を追加します.
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.1
hooks:
- id: flake8
# max-line-length setting is the same as black
# commit cannot be done when cyclomatic complexity is more than 10.
args: [--max-line-length, "88", --ignore=E402, --max-complexity, "10", --max-expression-complexity=7, --max-cognitive-complexity=7]
additional_dependencies: [flake8-bugbear, flake8-builtins, flake8-eradicate, pep8-naming, flake8-expression-complexity, flake8-cognitive-complexity]
markdownlint
Markdownのためのlinterです.こちらに記述されているルールを元に,よくない書き方に対して警告を出してくれます.
自分はmarkdownファイルにコマンドやその出力結果を記入することが多く,その際に1行あたりの文字数が規定を超えてしまうことが多いので,文字数の規制をなくすようなオプションを追加しています.
- repo: https://github.com/markdownlint/markdownlint
rev: master
hooks:
- id: markdownlint
# ignore line length of makrdownlint
args: [-r, ~MD013]
flake8
の拡張機能の紹介
flake8
には様々な拡張機能があります.
公開されている拡張機能は,awesome-flake8-extensionsにまとまっているので,参照してみてください.
以下では自分が気になった拡張機能について紹介します.
flake8-bugbear
flake8-bugbear
では.pyflake
やpycodestyle
で提供されていないような,より詳細なコードのチェックが可能になります.flake8-bugbear
の警告の一覧は,こちらを参照してください.
flake8-builtins
使用している変数や関数名が,組み込みオブジェクトの名前と被っていないかどうかをチェックします.
flake8-eradicate
コメントアウトされているコードを見つけて警告する拡張機能.
pep8-naming
変数や関数の命名がPEP8に準拠しているかどうかをチェックしてくれます.
flake8-expression-complexity
表現の複雑さを測定して警告を投げてくれる拡張機能です.expression complexityの正確な定義は分かりませんが,どうやらブーリアンの表現の複雑さを表しているようです.
flake8-cognitive-complexity
コードの認知的複雑度を計測し,警告を投げてくれる拡張機能です.認知的複雑度については,こちらの記事(Cognitive Complexity で、コードの読みやすさを定量的に計測しよう)が参考になります.
Bandit
pythonのコードにおけるセキュリティに関する問題をチェックしてくれる拡張機能です.
pandas-vet
pandas
を使ったコードのためのlinterです.
flake8-django
Django
を使ったコードのためにlinterです.
flake8-pytest-style
pytest
に関するコードスタイルをチェックしてくれる拡張機能です.
設定したhookを使ってみる
最終的な自分の設定は以下のようになりました.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: check-added-large-files
- id: check-json
- id: check-toml
- id: check-xml
- id: check-yaml
- id: debug-statements
- id: detect-aws-credentials
args: [--allow-missing-credentials]
- id: detect-private-key
- id: end-of-file-fixer
- id: name-tests-test
args: [--django] # to match `test*.py`.
# - id: no-commit-to-branch # to protect specific branches from direct checkins.
# args: [--branch, master]
- id: pretty-format-json
args: [--autofix]
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.1
hooks:
- id: flake8
# max-line-length setting is the same as black
# commit cannot be done when cyclomatic complexity is more than 10.
args: [--max-line-length, "88", --ignore=E402, --max-complexity, "10", --max-expression-complexity=7, --max-cognitive-complexity=7]
additional_dependencies: [flake8-bugbear, flake8-builtins, flake8-eradicate, pep8-naming, flake8-expression-complexity, flake8-cognitive-complexity]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.790
hooks:
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/isort
rev: 5.5.2
hooks:
- id: isort
args: ["--profile", "black"]
# for docstrings in python codes
- repo: https://github.com/myint/docformatter
rev: master
hooks:
- id: docformatter
args: [--in-place]
# for markdown
- repo: https://github.com/markdownlint/markdownlint
rev: master # or specific git tag
hooks:
- id: markdownlint
# ignore line length of makrdownlint
args: [-r, ~MD013]
この設定を使って,pre-commit
が実行されるかどうかを確認してみます.
試しに以下のようなダメダメなコードをcommitしようとしてみます.
import pandas as pd
import numpy as np
def hoge(a:str, b={})->None:
for i in range(10):
for j in range(10):
for k in range(10):
if i==0:
print(a+i)
if j==8:
print("hoge")
if i==j:
pass
elif i==j+1:
pass
elif i==j+2:
pass
for l in range(10):
if l == 5:
print("fuga")
コミットしてみます.すると多くの警告が出るのが確認できるかと思います.
$ git add sample.py
$ git commit -m "pre-commit test"
Check for added large files..............................................Passed
Check JSON...........................................(no files to check)Skipped
Check Toml...........................................(no files to check)Skipped
Check Xml............................................(no files to check)Skipped
Check Yaml...............................................................Passed
Debug Statements (Python)................................................Passed
Detect AWS Credentials...................................................Passed
Detect Private Key.......................................................Passed
Fix End of Files.........................................................Passed
Tests should end in _test.py.........................(no files to check)Skipped
Pretty format JSON...................................(no files to check)Skipped
flake8...................................................................Failed
- hook id: flake8
- exit code: 1
sample.py:1:1: F401 'pandas as pd' imported but unused
sample.py:2:1: F401 'numpy as np' imported but unused
sample.py:3:1: E302 expected 2 blank lines, found 0
sample.py:3:1: C901 'hoge' is too complex (11)
sample.py:3:1: CCR001 Cognitive complexity is too high (33 > 7)
sample.py:3:11: E231 missing whitespace after ':'
sample.py:3:19: B006 Do not use mutable data structures for argument defaults. They are created during function definition time. All calls to the function reuse this one instance of that data structure, persisting changes between them.
sample.py:3:22: E225 missing whitespace around operator
sample.py:6:17: B007 Loop control variable 'k' not used within the loop body. If this is intended, start the name with an underscore.
sample.py:7:21: E225 missing whitespace around operator
sample.py:8:28: E226 missing whitespace around arithmetic operator
sample.py:10:21: E225 missing whitespace around operator
sample.py:13:21: E225 missing whitespace around operator
sample.py:15:23: E225 missing whitespace around operator
sample.py:15:26: E226 missing whitespace around arithmetic operator
sample.py:17:23: E225 missing whitespace around operator
sample.py:17:26: E226 missing whitespace around arithmetic operator
sample.py:20:21: E741 ambiguous variable name 'l'
sample.py:21:24: E741 ambiguous variable name 'l'
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1
sample.py:8: error: Unsupported operand types for + ("str" and "int")
Found 1 error in 1 file (checked 1 source file)
black....................................................................Failed
- hook id: black
- files were modified by this hook
reformatted sample.py
All done! ✨ 🍰 ✨
1 file reformatted.
isort....................................................................Failed
- hook id: isort
- files were modified by this hook
Fixing /Users/yuchi/Documents/hoge_github/sample.py
docformatter.............................................................Passed
Markdownlint.........................................(no files to check)Skipped
flake8
やmypy
からの警告が出ているのが確認できました.
また,black
やisort
がコードをフォーマットしてくれるのも確認できます.flake8
やmypy
からの警告を修正してgit add sample.py
->git commit
することで,コミットを通すことができます.
試しに適切なコードをコミットしてみます.
def hoge(a: int, b: int) -> int:
return a + b
$ git add sample.py
$ git commit -m "pre-commit test"
Check for added large files..............................................Passed
Check JSON...........................................(no files to check)Skipped
Check Toml...........................................(no files to check)Skipped
Check Xml............................................(no files to check)Skipped
Check Yaml...............................................................Passed
Debug Statements (Python)................................................Passed
Detect AWS Credentials...................................................Passed
Detect Private Key.......................................................Passed
Fix End of Files.........................................................Passed
Tests should end in _test.py.........................(no files to check)Skipped
Pretty format JSON...................................(no files to check)Skipped
flake8...................................................................Passed
mypy.....................................................................Passed
black....................................................................Passed
isort....................................................................Passed
docformatter.............................................................Passed
Markdownlint.........................................(no files to check)Skipped
無事コミットが通りました!
このような感じでpre-commit
は,コミット時に様々なhookを設定して,コードを自動でチェックしたり,フォーマットすることができます.
皆さんもぜひpre-commit
を有効活用してみてください.
Discussion