これは何?
- VSCodeでのPythonのLint、Formatterの設定方法
- GitHub ActionsでのLintチェックの導入方法
-
pre-commit
を使ったLintチェックの設定方法
Goal (Where)
- チーム全員が共通のコードフォーマットにて開発できる。
- 低レベルなバグや不要なコードに気が付きやすくする。
- そのための方法はできるだけ自動化されている。
目的 (Why)
- 静的解析を行うことで、低レベルなバグやミスの発生を抑える。
- 低レベルなバグ: 未使用のimportや構文ミスなど
- チームの全員が共通のルールでコードを書くことにより、コードチェックのコストを減らす。
- コードフォーマットについて考える、作業する工数を減らす。
環境
- Python 3.9 (Django)
- VSCode
前提
Lintとは?
lint とは、主にC言語のソースコードに対し、コンパイラよりも詳細かつ厳密なチェックを行なうプログラムである。静的解析ツールとも呼ばれる。以下に挙げるように、コンパイラではチェックされないが、バグの原因になるような曖昧な記述についても警告される。構文(シンタックス)レベルのチェックだけでなく、意味(セマンティクス)レベルのチェックまで実行するものもある。
Wikipedia lint (https://ja.wikipedia.org/wiki/Lint)
フォーマッターとは?
コンピュータプログラムの開発ツールの一種で、開発者がプログラミング言語で記述したソースコードを、一定のルールに従って整形してくれるものをフォーマッターという。
同じ動作を行うプログラムでも、行頭にどのくらいスペースを空けるかなど、書き方には無数のバリエーションがあり、人によって好みの書き方が異なることも多い。フォーマッターはコードの書き方に一定の決まり(コーディング規約)を設け、これに従ってコードの意味を変えずに書式を調整する。
IT用語辞典 フォーマッター (https://e-words.jp/w/フォーマッター.html)
- Lintは元々C言語用に作成された静的解析ツールであり、現在は他言語のコードに対して構文チェックや静的解析を行なうプログラムも含めて lintと呼ぶ。
- 意味上は、lintはコード構文などの静的解析を行うことで不要なコードの検知や低レベルのバグの防止を行い、フォーマッターはコードフォーマットを整形する役目を持つ。
簡単に調べてみましたが、一言で lint
と言ってもパッケージによって実行できる範囲は違うため、あまり言葉にとらわれず、必要な機能を軸に考えた方がいい気もしました。
行ったこと
-
設定においては大きく2つを行いました。
- VSCodeにおける設定
- GitHub Actionsでの設定
-
設定したパッケージ
- black: PEP8に準拠したフォーマッター (https://black.readthedocs.io/en/stable/)
- flake8: PEP8に準拠したリンター (https://flake8.pycqa.org/en/latest/)
- isort: PEP8に準拠したimportのソートを行うフォーマッター (https://pycqa.github.io/isort/)
基本的には全員が最初から、IDEにて共通の設定をしていれば問題ない気もしましたが、プロジェクトが進んでいく間に、未フォーマットのコードが紛れ込む可能性もありそのため、GitHub ActionsでもLintチェックを行うこととしました。
VSCodeでの設定
上記の3つは全て、VSCode
のExtensionが用意されているため、もしチームがVSCode
を使用していればすぐに導入することができるようになります。未導入の人には導入を促すポップアップが出るように、rootディレクトリに以下ファイルを作成、設定しました。
{
"recommendations": [
"ms-python.python",
"ms-python.flake8",
"ms-python.black",
"ms-python.vscode-pylance",
"ms-python.isort"
]
}
またExtensionsの設定を .vscode/settings.json
に設定しています。
Djangoのため、いくつかのファイルを対象外としています。
{
"python.languageServer": "Pylance",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"black-formatter.args": ["--line-length", "120", "--force-exclude", "(migrations|deploy)"],
"isort.args": ["--profile", "black", "--extend-skip", "migrations", "--extend-skip", "deploy"],
"flake8.args": [
"--max-line-length=120",
"--ignore=E203,W503,W504",
"--extend-exclude=migrations,deploy"
]
}
GitHub Workflowの設定
今回はPRの作成、更新、再開に合わせてVSCode
の設定と同じフォーマットチェックをし、フォーマット整形すべき箇所があれば、 FAILEDさせるようにしたいと思います。
name: Lint
on:
workflow_dispatch:
pull_request:
branches: []
types: [opened, synchronize, reopened]
jobs:
build:
runs-on: ubuntu-20.04
env:
MAX_LINE_LENGTH: 120
strategy:
max-parallel: 4
steps:
- name: Set up Python 3.9.6
uses: actions/setup-python@v5
with:
python-version: '3.9.6'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install black flake8 isort
- name: Checkout to PR branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
- name: Check diff files and lint
run: |
DIFF_FILES=$(git diff --name-only --diff-filter=d ${{ github.base_ref}} HEAD | (grep '\.py$' || true))
if [ -z "$DIFF_FILES" ]; then
echo "No files to lint"
exit 0
fi
echo "Running flake8 lint check for only changed files"
for FILE in $DIFF_FILES; do
flake8 $FILE || (echo "flake8 found issues in $FILE" && exit 1)
done
echo "Running black format check for all files"
black . --check \
--line-length=${MAX_LINE_LENGTH} \
--force-exclude="(migrations|deploy)" || (echo "black found issues." && exit 1)
echo "Running isort format check for all files"
isort . --check --profile="black" \
--extend-skip="migrations" \
--extend-skip="deploy" || (echo "isort found issues." && exit 1)
flake8
の設定は別でファイルを用意して設定を行いました。
[flake8]
max-line-length = 120
extend-ignore = E203,W503,W504
extend-exclude = node_modules,__init__.py,migrations,deploy,.venv
検証
設定がうまくできているかの検証です。以下2点で問題ないことを確認しました。
- VSCodeではファイル保存時に、自動的に
black
,isort
によるフォーマットがされること。- 設定した対象外ファイルではフォーマットがされないこと。
- GitHub Actionsにて動作していること。
補足
場合によっては、GitHub Actions
を使用できないこともあると思います。自分のプロジェクトでも一時的に無料枠がなくなり、月を跨ぐまで使用できなくなる事態が起こりました。その際は、localでのcommit時に動かす pre-commit
が解決策となりました。
パッケージインストール
チーム開発で他の人とバージョンを合わせるため requirements.txt
ファイルに記載することが多いかと思います。
# Formatting and linting
black~=24.4.0
flake8~=7.0.0
isort~=5.13.0
pip install -r requirements.txt
pre-commit スクリプト作成、設定
- 以下ファイルを
root
ディレクトリに作成し、./pre-commit install
を実施。- その後は
git
にてコミットコマンドを実行するたびに、スクリプトが実行され、成功したらcommit
、失敗したらcommit
しないという動作が実現できます。
- その後は
#!/usr/bin/env bash
# This is a pre-commit hook that validates code formatting.
#
# Install this by running the script with an argument of "install",
# which installs a symlink to .git/hooks/precommit:
# $ ln -s ../../hooks/pre-commit .git/hooks/pre-commit
root="$(git rev-parse --show-toplevel 2>/dev/null)"
set -e
# Some sanity checking.
[[ -n "$root" ]]
# Installation.
if [[ "$1" == "install" ]]; then
hook="$root"/.git/hooks/pre-commit
if [[ ! -e "$hook" ]]; then
ln -s ../../pre-commit "$hook"
echo "Installed git pre-commit hook at $hook"
else
echo "Hook already installed"
fi
exit
fi
save_merge_files() {
# Move MERGE_[HEAD|MODE|MSG] files to the root directory, and let `git stash push` save them.
for f in "$root"/.git/MERGE_*; do
if [[ -e "$f" ]]; then
t=$(basename $f)
mv -f "$f" "$t"
fi
done
}
restore_merge_files() {
# Moves MERGE files restored by `git stash pop` back into .git/ directory.
for f in MERGE_*; do
if [[ -e "$f" ]]; then
if [[ -e "${root}/.git/${f}" ]]; then
echo "Failed to restore ${f}. File already exists" 1>&2
else
mv -f "$f" "${root}/.git/${f}"
fi
fi
done
}
# Check formatting.
echo "Check formatting"
stashfile=$(mktemp .pre-commit.stashXXXXXX)
trap 'set +e;git stash pop -q; rm -f "$stashfile"; restore_merge_files' EXIT
save_merge_files
git stash push -k -u -q -m "pre-commit stash"
len=120
if ! errors=($(black --line-length=${len} --force-exclude="(node_modules|deploy|migrations)" --check .)); then
echo "Formatting errors found."
exit 1
fi
if ! errors=($(isort . --check --profile="black" --extend-skip=deploy --extend-skip=migrations)); then
echo "Import order errors found."
exit 1
fi
if ! errors=($(flake8)); then
echo "flake8 found issues."
exit 1
fi
echo "Run pre-commit checks"
check() {
msg="$1"
shift
if "$@"; then
echo "$msg: OK"
else
res=$?
echo "-----------------------"
echo "$msg: FAILED ($res)"
echo " $@"
exit $res
fi
}
最後に
ここまで読んでいただきありがとうございました。
Lint
やFormatter
を初めて設定してみましたが、自動化により細かいミスを防ぐことができ、また開発者も改行などの細かいコーディング規約を意識することなく、コーディングできるというメリットがあると思います。
他の言語でも基本的な設定方法は同じかと思うので、今後はかならず実施していきたいと思います。