🙄

Pythonプロジェクトでpre-commitを使ったコード品質管理

に公開

Pythonプロジェクトでコードの品質を保つために、どのようなツールを使っていますか?
手動でフォーマッターやリンターを実行するのは面倒で、忘れてしまうこともありますよね。

今回は、pre-commitを使った自動的なコード品質管理の方法を紹介します。Gitコミット前に自動でチェックが実行されるため、品質の高いコードを維持できます。


🚨 問題:手動でのコード品質管理の課題

従来のコード品質管理では、以下のような課題がありました:

  • フォーマッターの実行忘れ: 手動で実行するため、忘れがち
  • リンターの見落とし: エラーがあってもコミットしてしまう
  • チーム間の統一性: 人によって実行するツールが違う
  • 時間の無駄: コミット後にCIでエラーが出て修正が必要
  • 設定の散在: 各ツールの設定ファイルがバラバラ

🆕 解決策:pre-commitによる自動化

pre-commitを使うことで、Gitコミット前に自動的にコード品質チェックが実行されます。

pre-commitとは

  • Gitフックを簡単に管理するツール
  • コミット前に自動でチェックを実行
  • 複数のツールを統一的に管理
  • 設定が簡単で、チーム全体で共有可能

🛠 方法1:基本的なpre-commitのセットアップ

まずは、Poetryプロジェクトにpre-commitを導入する方法を紹介します。

1. pre-commitのインストール

# Poetryプロジェクトでpre-commitを追加
poetry add --group dev pre-commit

# または直接インストール
pip install pre-commit

2. .pre-commit-config.yamlの作成

プロジェクトのルートに.pre-commit-config.yamlファイルを作成します。

repos:
  # コードフォーマッター
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black
        language_version: python3

  # import文の整理
  - repo: https://github.com/pre-commit/mirrors-isort
    rev: v5.12.0
    hooks:
      - id: isort
        args: ["--profile", "black"]

  # 高速なリンター
  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]

  # 型チェック(時間がかかる場合は後で実行)
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.5.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

3. pre-commitフックのインストール

# Gitフックをインストール
poetry run pre-commit install

# 既存のファイルに対して実行
poetry run pre-commit run --all-files

🔄 方法2:pyproject.tomlとの連携

pre-commitで使用するツールの設定をpyproject.tomlに集約します。

1. 完全なpyproject.toml設定

[tool.poetry]
name = "my-python-project"
version = "0.1.0"
description = "A Python project with pre-commit"
authors = ["Your Name <your.email@example.com>"]

[tool.poetry.dependencies]
python = "^3.8"
requests = "^2.31.0"

[tool.poetry.group.dev.dependencies]
pre-commit = "^3.5.0"
black = "^24.3.0"
isort = "^5.12.0"
ruff = "^0.4.0"
mypy = "^1.5.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

# Black設定
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
  # directories
  \.eggs
  | \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | build
  | dist
)/
'''

# isort設定
[tool.isort]
profile = "black"
multi_line_output = 3
line_length = 88
known_first_party = ["my_python_project"]

# Ruff設定
[tool.ruff]
line-length = 88
target-version = "py38"
select = [
    "E",  # pycodestyle errors
    "W",  # pycodestyle warnings
    "F",  # pyflakes
    "I",  # isort
    "B",  # flake8-bugbear
    "C4", # flake8-comprehensions
    "UP", # pyupgrade
]
ignore = [
    "E203",  # whitespace before ':'
    "W503",  # line break before binary operator
    "B008",  # do not perform function calls in argument defaults
]

[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"]

# MyPy設定
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true

[[tool.mypy.overrides]]
module = [
    "requests.*",
    "urllib3.*",
]
ignore_missing_imports = true

2. 最適化された.pre-commit-config.yaml

repos:
  # コードフォーマッター
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black
        language_version: python3

  # import文の整理
  - repo: https://github.com/pre-commit/mirrors-isort
    rev: v5.12.0
    hooks:
      - id: isort
        args: ["--profile", "black"]

  # 高速なリンター(black/isortと競合しない設定)
  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]

  # 型チェック(時間がかかるため、必要に応じて)
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.5.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]
        # 型チェックが重い場合は、手動で実行することも可能
        # stages: [manual]

📊 方法3:ツールの比較と選択

Pythonのコード品質ツールには様々な選択肢があります。プロジェクトの規模や要件に応じて選択しましょう。

ツール 目的 速度 設定の複雑さ 推奨度
black コード整形 高速 簡単 ⭐⭐⭐⭐⭐
isort import整理 高速 簡単 ⭐⭐⭐⭐⭐
ruff リント 超高速 中程度 ⭐⭐⭐⭐⭐
flake8 リント 中速 簡単 ⭐⭐⭐⭐
mypy 型チェック 低速 複雑 ⭐⭐⭐⭐
pylint 高機能リント 低速 複雑 ⭐⭐⭐

最小構成(推奨)

repos:
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black

  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix]

完全構成(大規模プロジェクト)

repos:
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black

  - repo: https://github.com/pre-commit/mirrors-isort
    rev: v5.12.0
    hooks:
      - id: isort
        args: ["--profile", "black"]

  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.5.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

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

✅ 方法4:実際の運用事例

実際のプロジェクトでの運用例を紹介します。

1. 基本的なPythonアプリケーション

# src/my_app/main.py
from typing import List, Optional
import requests
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: Optional[str] = None

class UserService:
    def __init__(self, api_url: str) -> None:
        self.api_url = api_url

    def get_users(self) -> List[User]:
        """ユーザー一覧を取得する"""
        response = requests.get(f"{self.api_url}/users")
        response.raise_for_status()
        
        users_data = response.json()
        return [User(**user_data) for user_data in users_data]

    def create_user(self, name: str, email: Optional[str] = None) -> User:
        """新しいユーザーを作成する"""
        user_data = {"name": name}
        if email:
            user_data["email"] = email
            
        response = requests.post(f"{self.api_url}/users", json=user_data)
        response.raise_for_status()
        
        return User(**response.json())

2. テストコード

# tests/test_main.py
import pytest
from unittest.mock import Mock, patch
from my_app.main import User, UserService

def test_user_creation():
    """ユーザー作成のテスト"""
    user = User(id=1, name="Test User", email="test@example.com")
    assert user.id == 1
    assert user.name == "Test User"
    assert user.email == "test@example.com"

def test_user_service_get_users():
    """UserService.get_usersのテスト"""
    mock_response = Mock()
    mock_response.json.return_value = [
        {"id": 1, "name": "User 1"},
        {"id": 2, "name": "User 2"},
    ]
    
    with patch("requests.get", return_value=mock_response):
        service = UserService("http://api.example.com")
        users = service.get_users()
        
        assert len(users) == 2
        assert users[0].name == "User 1"
        assert users[1].name == "User 2"

3. 開発ワークフロー

# 1. 新しい機能ブランチを作成
git checkout -b feature/user-management

# 2. コードを書く
# ... コードを編集 ...

# 3. コミットを試行(pre-commitが自動実行される)
git add .
git commit -m "Add user management feature"

# 4. もしpre-commitでエラーが出た場合
# 自動修正されるか、手動で修正して再度コミット

# 5. 手動でpre-commitを実行することも可能
poetry run pre-commit run --all-files

# 6. 特定のフックのみ実行
poetry run pre-commit run black --all-files
poetry run pre-commit run ruff --all-files

🔧 方法5:トラブルシューティング

よくある問題とその解決方法を紹介します。

1. pre-commitが遅い場合

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black
        # 特定のファイルのみ対象にする
        files: \.py$
        # 並列実行を有効にする
        parallel: true

2. 型チェックが重い場合

# mypyを手動実行に変更
- repo: https://github.com/pre-commit/mirrors-mypy
  rev: v1.5.0
  hooks:
    - id: mypy
      stages: [manual]  # 手動実行のみ

3. 特定のファイルを除外する

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black
        exclude: ^(migrations|generated_files)/

4. CI/CDでの実行

# .github/workflows/pre-commit.yml
name: Pre-commit

on: [push, pull_request]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.8'
          
      - name: Install Poetry
        run: |
          curl -sSL https://install.python-poetry.org | python3 -
          
      - name: Install dependencies
        run: poetry install
        
      - name: Run pre-commit
        run: poetry run pre-commit run --all-files

🧭 おわりに

pre-commitを使うことで、Pythonプロジェクトのコード品質管理が格段に楽になります。

導入のメリット

  • 自動化: 手動での実行が不要
  • 一貫性: チーム全体で同じ品質基準
  • 効率性: コミット前の早期エラー発見
  • 学習効果: 良いコードの書き方を自然に身につける

推奨する導入順序

  1. 最小構成から始める: black + ruff
  2. 段階的に追加: isort → mypy
  3. チームに共有: .pre-commit-config.yamlをコミット
  4. CI/CDと連携: GitHub Actionsなどで自動実行

次のステップ

  1. 既存プロジェクトにpre-commitを導入
  2. チームメンバー全員にセットアップ方法を共有
  3. CI/CDパイプラインでの自動実行を設定
  4. 定期的にツールの設定を見直し

pre-commitは最初の設定に少し時間がかかりますが、一度導入すれば開発効率が大幅に向上します。ぜひ試してみてください!

Discussion