第4章:プロジェクト設計

更新日:2025年12月9日

本章では、保守性・拡張性の高いPythonプロジェクトの設計手法を解説する。標準的なディレクトリ構成、pyproject.tomlによるプロジェクト設定、pytestを中心としたテスト戦略、GitHub Actionsを用いたCI/CDパイプライン、MkDocsによるドキュメント生成について学ぶ。適切なプロジェクト構造は、長期的な開発効率とチーム協業に大きく寄与する。

1. ディレクトリ構成

Pythonプロジェクトのディレクトリ構成には主に2つのパターンがある。

1.1 srcレイアウト:パッケージをsrc/ディレクトリに配置するパターン。PyPA(Python Packaging Authority)が推奨[1]。

myproject/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── core.py
│       ├── utils.py
│       └── cli.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_core.py
│   └── test_utils.py
├── docs/
│   └── index.md
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore

1.2 flatレイアウト:パッケージをルート直下に配置するパターン。小規模プロジェクトで使用される。

myproject/
├── mypackage/
│   ├── __init__.py
│   ├── core.py
│   └── utils.py
├── tests/
│   └── test_core.py
├── pyproject.toml
└── README.md

Table 1. srcレイアウト vs flatレイアウト

観点 srcレイアウト flatレイアウト
インストール要否 必要(editable install) 不要(直接import可能)
テスト信頼性 高(インストール後のコードをテスト) 低(ローカルコードをテスト)
名前空間 クリーン ルートと混在リスク
推奨用途 ライブラリ、中〜大規模 スクリプト、小規模

1.3 アプリケーション向け構成:Webアプリケーション等では機能別のモジュール分割が有効。

myapp/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── main.py           # エントリーポイント
│       ├── config.py         # 設定管理
│       ├── api/              # APIエンドポイント
│       │   ├── __init__.py
│       │   ├── routes.py
│       │   └── dependencies.py
│       ├── models/           # データモデル
│       │   ├── __init__.py
│       │   └── user.py
│       ├── services/         # ビジネスロジック
│       │   ├── __init__.py
│       │   └── user_service.py
│       └── repositories/     # データアクセス
│           ├── __init__.py
│           └── user_repository.py
├── tests/
├── alembic/                  # DBマイグレーション
├── pyproject.toml
├── Dockerfile
└── docker-compose.yml

Fig. 1に構成選択のフローを示す。

2. pyproject.toml詳解

pyproject.tomlはPEP 518/621で標準化されたプロジェクト設定ファイルである[2]。ビルドシステム、依存関係、ツール設定を一元管理できる。

2.1 基本構造

# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mypackage"
version = "0.1.0"
description = "A sample Python package"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
    {name = "Your Name", email = "you@example.com"}
]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]
keywords = ["sample", "python"]

dependencies = [
    "requests>=2.28.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "ruff>=0.1.0",
    "mypy>=1.0.0",
]
docs = [
    "mkdocs>=1.5.0",
    "mkdocs-material>=9.0.0",
]

[project.scripts]
mypackage = "mypackage.cli:main"

[project.urls]
Homepage = "https://github.com/yourname/mypackage"
Documentation = "https://yourname.github.io/mypackage"
Repository = "https://github.com/yourname/mypackage"

2.2 ビルドバックエンド:Table 2に主要なビルドバックエンドを示す。

Table 2. ビルドバックエンドの比較

バックエンド 特徴 用途
hatchling 高速、シンプル、モダン 一般的なパッケージ
setuptools 伝統的、高機能 C拡張、レガシー
poetry-core Poetry統合 Poetryプロジェクト
flit-core 最小限、高速 純粋Pythonパッケージ
maturin Rust拡張対応 PyO3プロジェクト

2.3 ツール設定セクション:各ツールの設定を統合できる。

# ツール設定
[tool.ruff]
target-version = "py312"
line-length = 88

[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP"]

[tool.mypy]
python_version = "3.12"
strict = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=src/mypackage --cov-report=term-missing"

[tool.coverage.run]
source = ["src/mypackage"]
branch = true

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
]

3. テスト戦略

3.1 pytest

pytestはPythonの事実上の標準テストフレームワークである[3]。シンプルな記法と強力なプラグインエコシステムを持つ。

# tests/test_core.py
import pytest
from mypackage.core import Calculator

class TestCalculator:
    """Calculatorクラスのテスト"""

    def test_add(self):
        calc = Calculator()
        assert calc.add(2, 3) == 5

    def test_divide(self):
        calc = Calculator()
        assert calc.divide(10, 2) == 5.0

    def test_divide_by_zero(self):
        calc = Calculator()
        with pytest.raises(ZeroDivisionError):
            calc.divide(10, 0)

    @pytest.mark.parametrize("a,b,expected", [
        (1, 1, 2),
        (0, 0, 0),
        (-1, 1, 0),
        (100, 200, 300),
    ])
    def test_add_parametrized(self, a, b, expected):
        calc = Calculator()
        assert calc.add(a, b) == expected

3.1.1 フィクスチャ:テストのセットアップ・ティアダウンを管理。

# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture
def db_session():
    """テスト用データベースセッション"""
    engine = create_engine("sqlite:///:memory:")
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()

@pytest.fixture
def sample_user():
    """サンプルユーザーデータ"""
    return {
        "name": "Test User",
        "email": "test@example.com",
        "age": 25
    }

# スコープ指定(session: テスト全体で1回)
@pytest.fixture(scope="session")
def api_client():
    """APIクライアント(セッション全体で共有)"""
    from mypackage.client import APIClient
    client = APIClient()
    yield client
    client.close()

3.1.2 モックとパッチ:外部依存をテストから分離。

# tests/test_service.py
from unittest.mock import Mock, patch
from mypackage.services import UserService

def test_get_user_from_api():
    # Mockオブジェクトの使用
    mock_client = Mock()
    mock_client.get.return_value = {"id": 1, "name": "Alice"}

    service = UserService(client=mock_client)
    user = service.get_user(1)

    assert user["name"] == "Alice"
    mock_client.get.assert_called_once_with("/users/1")

@patch("mypackage.services.requests.get")
def test_fetch_external_data(mock_get):
    # patchデコレータによるモック
    mock_get.return_value.json.return_value = {"data": "test"}
    mock_get.return_value.status_code = 200

    from mypackage.services import fetch_data
    result = fetch_data("https://api.example.com")

    assert result == {"data": "test"}

3.2 カバレッジ

pytest-covによりテストカバレッジを測定できる。

# テスト実行とカバレッジ測定
pytest --cov=src/mypackage --cov-report=html

# カバレッジレポートの確認
open htmlcov/index.html

3.2.1 カバレッジ目標:Table 3にカバレッジ目標の目安を示す。

Table 3. カバレッジ目標の目安

カバレッジ 評価 備考
90%以上 優秀 重要なライブラリ向け
80-90% 良好 一般的なプロジェクト目標
70-80% 許容 最低限の品質保証
70%未満 要改善 テスト追加を検討

3.2.2 カバレッジ100%を目指すことは必ずしも効率的ではない。重要なビジネスロジックとエッジケースに集中することが推奨される。

4. CI/CDパイプライン

GitHub Actionsによる継続的インテグレーション/デリバリーの設定例を示す[4]。

4.1 基本的なCIワークフロー

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v4

      - name: Set up Python ${{ matrix.python-version }}
        run: uv python install ${{ matrix.python-version }}

      - name: Install dependencies
        run: uv sync --all-extras

      - name: Run linter
        run: uv run ruff check .

      - name: Run formatter check
        run: uv run ruff format --check .

      - name: Run type checker
        run: uv run mypy src/

      - name: Run tests
        run: uv run pytest --cov=src/ --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: coverage.xml

4.2 リリースワークフロー:タグ作成時にPyPIへ自動公開。

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - "v*"

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # Trusted publishing

    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v4

      - name: Build package
        run: uv build

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        # Trusted publishingにより認証情報不要

4.3 ブランチ戦略との連携:Fig. 2にCI/CDフローを示す。

5. ドキュメント生成

MkDocsはMarkdownベースのドキュメントサイトジェネレータである[5]。mkdocs-materialテーマにより高品質なドキュメントサイトを構築できる。

5.1 セットアップ

# インストール
uv add mkdocs mkdocs-material mkdocstrings[python] --dev

# プロジェクト初期化
mkdocs new .

# ローカルサーバー起動
mkdocs serve

# ビルド
mkdocs build

5.2 mkdocs.yml設定

# mkdocs.yml
site_name: MyPackage Documentation
site_url: https://yourname.github.io/mypackage/
repo_url: https://github.com/yourname/mypackage
repo_name: yourname/mypackage

theme:
  name: material
  palette:
    - scheme: default
      primary: indigo
      accent: indigo
      toggle:
        icon: material/brightness-7
        name: ダークモードに切替
    - scheme: slate
      primary: indigo
      accent: indigo
      toggle:
        icon: material/brightness-4
        name: ライトモードに切替
  features:
    - navigation.instant
    - navigation.tabs
    - content.code.copy

plugins:
  - search
  - mkdocstrings:
      handlers:
        python:
          options:
            docstring_style: google

nav:
  - Home: index.md
  - Getting Started:
    - Installation: getting-started/installation.md
    - Quick Start: getting-started/quickstart.md
  - User Guide:
    - Configuration: guide/configuration.md
    - Usage: guide/usage.md
  - API Reference: api/reference.md
  - Contributing: contributing.md

markdown_extensions:
  - pymdownx.highlight:
      anchor_linenums: true
  - pymdownx.superfences
  - admonition
  - toc:
      permalink: true

5.3 docstringからAPIドキュメント生成:mkdocstringsによりソースコードのdocstringを自動抽出。

# src/mypackage/core.py
class Calculator:
    """基本的な計算機能を提供するクラス。

    Attributes:
        precision: 計算結果の精度(小数点以下桁数)

    Example:
        >>> calc = Calculator(precision=2)
        >>> calc.add(1.111, 2.222)
        3.33
    """

    def __init__(self, precision: int = 10) -> None:
        """Calculatorを初期化する。

        Args:
            precision: 計算結果の精度。デフォルトは10。
        """
        self.precision = precision

    def add(self, a: float, b: float) -> float:
        """2つの数値を加算する。

        Args:
            a: 第1の数値
            b: 第2の数値

        Returns:
            aとbの和(precision桁で丸め)

        Raises:
            TypeError: 数値以外が渡された場合
        """
        return round(a + b, self.precision)
# docs/api/reference.md
# API Reference

::: mypackage.core.Calculator
    options:
      show_source: true

5.4 GitHub Pagesへのデプロイ

# .github/workflows/docs.yml
name: Deploy Docs

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v4
      - run: uv sync --group docs
      - run: uv run mkdocs gh-deploy --force

References

[1] PyPA, "Python Packaging User Guide - src layout," packaging.python.org, 2024.

[2] P. Ganssle et al., "PEP 621 -- Storing project metadata in pyproject.toml," python.org, 2020.

[3] pytest, "pytest: helps you write better programs," docs.pytest.org, 2024.

[4] GitHub, "GitHub Actions Documentation," docs.github.com, 2024.

[5] MkDocs, "MkDocs - Project documentation with Markdown," mkdocs.org, 2024.

免責事項
本コンテンツは2025年12月時点の情報に基づいて作成されている。GitHub ActionsやPyPIのAPIは変更される可能性があるため、最新の公式ドキュメントを参照されたい。

← 前章:開発環境とツールチェーン次章:データ処理 →