diff --git a/.claude/skills/python-uv-ci-setup/SKILL.md b/.claude/skills/python-uv-ci-setup/SKILL.md index d78a48a..7175b75 100644 --- a/.claude/skills/python-uv-ci-setup/SKILL.md +++ b/.claude/skills/python-uv-ci-setup/SKILL.md @@ -1,6 +1,6 @@ --- name: python-uv-ci-setup -description: uv を使う Python プロジェクトで、format/lint/静的型チェック/テスト/docstring ルールをローカルと GitHub Actions で一貫運用するためのセットアップPlaybook。`pyproject.toml` の `[dependency-groups]`、`.pre-commit-config.yaml`、`.github/workflows/ci.yml` を新規作成または更新し、`uv run pre-commit install` まで完了させる依頼で使う。 +description: uv を使う Python プロジェクトで、format/lint/静的型チェック/依存方向の検証/依存の衛生/テスト/docstring ルールをローカルと GitHub Actions で一貫運用するためのセットアップPlaybook。`pyproject.toml` の `[dependency-groups]` と `[tool.importlinter]`、`.pre-commit-config.yaml`、`.github/workflows/ci.yml`、`.github/dependabot.yml` を新規作成または更新し、`uv run pre-commit install` まで完了させる依頼で使う。 --- @@ -8,35 +8,41 @@ description: uv を使う Python プロジェクトで、format/lint/静的型 # Python uv CIセットアップ -このPlaybookでは、`uv + ruff + mypy + pytest + pre-commit + GitHub Actions` を最小差分で導入し、ローカルとCIの品質ゲートをそろえる。 +このPlaybookでは、`uv + ruff + mypy + import-linter + deptry + pytest + pre-commit + GitHub Actions` を最小差分で導入し、ローカルとCIの品質ゲートをそろえる。 ## 実行フロー 1. 前提を確認する。 -- ルートに `pyproject.toml` があるか確認する。なければ `uv init` を提案する。 +- ルートに `pyproject.toml` があるか確認する。なければ `uv init --package --build-backend uv` を提案する。 +- `pyproject.toml` に `[build-system]` があるか確認する。src レイアウトでは必須(無いと `src/` 配下を mypy / import-linter / pytest が解決できない)。 - `uv --version` と `python --version` を確認する。 - Git管理下か確認する。未初期化なら `git init` を実行してから進む。 2. 既存設定を監査する。 -- `pyproject.toml` の `[dependency-groups]`、`[tool.ruff]`、`[tool.mypy]`、`[tool.pytest.*]` を確認する。 -- `.pre-commit-config.yaml` と `.github/workflows/*.yml` を確認する。 +- `pyproject.toml` の `[dependency-groups]`、`[tool.ruff]`、`[tool.mypy]`、`[tool.pytest.*]`、`[tool.coverage.*]`、`[tool.importlinter]`、`[tool.deptry]` を確認する。 +- `.pre-commit-config.yaml`、`.github/workflows/*.yml`、`.github/dependabot.yml` を確認する。 - 既存設定がある場合は上書きせず、重複を避けて統合する。 3. `pyproject.toml` を `uv` 前提で整備する。 - 開発依存を `dependency-groups.dev` に集約する。 -- 最低限の開発依存をそろえる: `ruff`, `mypy`, `pytest`, `pre-commit`。 +- 最低限の開発依存をそろえる: `ruff`, `mypy`, `import-linter`, `deptry`, `pytest`, `pytest-cov`, `pre-commit`。 - ルールは `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md` の `pyproject.toml` テンプレートを基準にし、既存プロジェクトに合わせて微調整する。 +- `[tool.importlinter]` の `your_project` を実際のパッケージ名に置換する。まだ無い層は `"(ports)"` のように括弧で囲んで省略可能にする。 +- `forbidden_modules` にプロジェクトで使う外部技術(Web フレームワーク、DB / HTTP クライアント)を足す。 4. pre-commit を設定する。 - `.pre-commit-config.yaml` を作成または更新する。 +- `pre-commit-hooks` の基本フック(秘密鍵・巨大ファイル・TOML/YAML 構文・行末)を入れる。 - `uv-pre-commit` の `uv-lock` を入れてロックファイル整合を強制する。 -- `uv run` 経由で `ruff format --check`、`ruff check`、`mypy` を実行する。 +- `uv run` 経由で `ruff format --check`、`ruff check`、`mypy`、`lint-imports`、`deptry` を実行する。 - `pytest` は既定で `pre-push` に配置して開発体験を維持する。全コミットで必須にしたい場合は `stages` を `pre-commit` に変更する。 -5. GitHub Actions を設定する。 +5. GitHub Actions と Dependabot を設定する。 - `.github/workflows/ci.yml` を作成または更新する。 -- `actions/setup-python` と `astral-sh/setup-uv` を使い、`uv sync --locked --dev` の後に同等チェックを実行する。 +- `permissions: contents: read` と `concurrency` を置く。 +- `actions/setup-python` と `astral-sh/setup-uv` を使い、`uv sync --locked --dev` の後にローカルと同じ 6 本のチェックを実行する。 - キャッシュは `setup-uv` の `enable-cache: true` を基本にする。 +- `.github/dependabot.yml` を作成し、`github-actions` と `uv` を週次更新にする。 6. ローカルセットアップを完了する。 - `uv lock` @@ -48,24 +54,33 @@ description: uv を使う Python プロジェクトで、format/lint/静的型 - `uv run ruff format --check .` - `uv run ruff check .` - `uv run mypy .` +- `uv run lint-imports` +- `uv run deptry src` - `uv run pytest -q` +- import-linter が層を見つけているか確かめる: `domain` から `adapters` を import する行をわざと 1 つ足し、`uv run lint-imports` が失敗することを確認してから戻す。契約が層を見つけられていないと静かに通ってしまうため、この確認を省略しない。 8. 結果を報告する。 - 追加・更新したファイル -- 実行コマンドと結果 -- 残課題(既存コード由来のlint/type/test失敗など) +- 実行コマンドと結果(逆依存で失敗した確認を含む) +- 残課題(既存コード由来のlint/type/test失敗、`fail_under` に届かないカバレッジなど) ## 運用ルール - 型チェックは `mypy` に固定し、`ty` は使わない。 +- 型ヒントは必須(`disallow_untyped_defs`)。テストにも同じ基準を適用し、override で緩めない。 +- 依存方向(`adapters -> application -> ports -> domain`)と `domain` / `ports` の外部技術への非依存は import-linter で検証する。文書の約束だけにしない。 +- import-linter の契約を緩める(`ignore_imports` を足す)ときは理由をコメントに残す。例外が増えるなら設計を見直す。 - docstring は Google style を採用し、短文 1 行のみの記述を避ける。 - docstring の先頭では「何をする処理か」「どの条件で使うか」を日本語で具体的に説明する。 - 引数がある処理は `Args`、戻り値がある処理は `Returns`、例外を送出しうる処理は `Raises` を記載する。 - `pydocstyle` の `convention = "google"` を有効化し、必要に応じて日本語運用に不要なルールのみ最小限で除外する。 - `project.requires-python` を定義し、Ruff のバージョン推論と整合させる。 +- カバレッジ閾値(`fail_under`)を下げるときは理由を ADR に残す。 - CI とローカルで実行コマンドを一致させる。 ## 参照ファイル - 設定方針と採用理由: `docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md` - そのまま適用できる雛形: `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md` +- 依存方向を機械検証する判断: `docs/adr/0002-enforce-hexagonal-dependencies-with-import-linter.md` +- 品質ゲート拡充の判断: `docs/adr/0003-expand-ci-quality-gates.md` diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a37bbee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/ai-context-sync.yml b/.github/workflows/ai-context-sync.yml index d2dacde..f2cbb10 100644 --- a/.github/workflows/ai-context-sync.yml +++ b/.github/workflows/ai-context-sync.yml @@ -32,10 +32,10 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.12" diff --git a/docs/adr/0002-enforce-hexagonal-dependencies-with-import-linter.md b/docs/adr/0002-enforce-hexagonal-dependencies-with-import-linter.md new file mode 100644 index 0000000..6927ff9 --- /dev/null +++ b/docs/adr/0002-enforce-hexagonal-dependencies-with-import-linter.md @@ -0,0 +1,47 @@ +# ADR 0002: Hexagonal の依存方向を import-linter で機械検証する + +最終更新: 2026-09-12 +- ステータス: 承認済み(accepted) +- 決定者: shogo-hs +- 関連: `docs/ai/canonical/coding-standards.md`, `docs/ai/canonical/playbooks/python-uv-ci-setup.md`, `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md`, [ADR 0003](./0003-expand-ci-quality-gates.md) + +## 1. 文脈 + +- 本テンプレートは Hexagonal Architecture(`adapters -> application -> ports -> domain`)を採用し、「domain は外部技術へ依存しない」を設計原則にしている。 +- しかし CI が回しているのは ruff / mypy / pytest だけで、依存方向は文書上の約束にとどまっていた。domain から adapters を import しても、ports に HTTP クライアントを持ち込んでも CI は通る。 +- AI エージェントが実装する前提のテンプレートなので、文書の約束より機械の網のほうが違反を止めやすい。 + +## 2. 決定 + +- [import-linter](https://import-linter.readthedocs.io/) を開発依存に加え、`pyproject.toml` の `[tool.importlinter]` に 2 つの契約を置く。 + - `layers` 契約: `adapters`, `application`, `ports`, `domain` の順で、上から下へだけ import を許す。 + - `forbidden` 契約: `domain` と `ports` から Web フレームワーク・DB / HTTP クライアント(`fastapi`, `sqlalchemy`, `httpx`, `requests`, `boto3` を初期値)への import を禁止する。`include_external_packages = true` で未インストールの名前も判定する。 +- `uv run lint-imports` を pre-commit と GitHub Actions の両方に入れ、ローカルと CI で同じ契約を検証する。 +- 導入時は逆方向の import をわざと 1 つ足して失敗することを確認してから戻す。契約が層を見つけられていないときは静かに通ってしまうため。 +- 優先した要件: 設計原則の違反を PR の段階で止めること。追加ツールは 1 本に抑えること。 + +## 3. 代替案と不採用理由 + +- 代替案A: 文書(`docs/rules/code_architecture/README.md`)とレビューで守る。 + - 不採用理由: 現状がこれで、違反を止められていない。レビューは人の注意力に依存する。 +- 代替案B: ruff の `flake8-tidy-imports`(`banned-api`)で禁止 import を列挙する。 + - 不採用理由: モジュール単位の禁止は書けるが、層の順序(推移的な依存を含む)は表現できない。 +- 代替案C: 自前のスクリプトで `ast` を歩いて import を検査する。 + - 不採用理由: import-linter が同じことを契約の宣言だけで行える。保守対象を増やさない。 +- 代替案D: pytest のテストとして依存方向を検査する(`pytest-archon` など)。 + - 不採用理由: 契約が Python コードに埋まり、設定として一覧できない。テストの失敗と設計違反が混ざる。 + +## 4. 影響 + +- コードへの影響: `src//` の各層に `__init__.py` が必要(bootstrap が生成する)。既存プロジェクトで逆依存があると CI が落ちるため、導入時に修正するか `ignore_imports` に理由つきで登録する。 +- 運用への影響: `pyproject.toml` に `[build-system]` が必要になる(無いとパッケージが解決できず契約が評価できない)。契約を緩めるときは理由をコメントに残す。 +- ドキュメントへの影響: `python-uv-ci-setup` Playbook、`templates.md`、`tooling-best-practices.md`、bootstrap が生成する `docs/rules/code_architecture/README.md`。 + +## 5. フォローアップ + +- [ ] テンプレートから立ち上げたプロジェクトで、`ignore_imports` が増えていないかを定期的に見る。 +- [ ] `forbidden_modules` の初期値が実プロジェクトの外部技術と合っているかを見直す。 + +## 6. 変更履歴 + +- 2026-09-12: 初版作成。 diff --git a/docs/adr/0003-expand-ci-quality-gates.md b/docs/adr/0003-expand-ci-quality-gates.md new file mode 100644 index 0000000..bcbfd72 --- /dev/null +++ b/docs/adr/0003-expand-ci-quality-gates.md @@ -0,0 +1,61 @@ +# ADR 0003: CI の品質ゲートを ruff / mypy / pytest の 3 本から 6 本へ拡充する + +最終更新: 2026-09-12 +- ステータス: 承認済み(accepted) +- 決定者: shogo-hs +- 関連: `docs/ai/canonical/playbooks/python-uv-ci-setup.md`, `docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md`, [ADR 0002](./0002-enforce-hexagonal-dependencies-with-import-linter.md) + +## 1. 文脈 + +- `python-uv-ci-setup` Playbook の品質ゲートは ruff format / ruff check / mypy / pytest だった。 +- 次の穴があった。 + - pytest-cov は dev 依存に入っているだけで、カバレッジの閾値が無い。 + - 未使用・未宣言の依存を見ていない。 + - mypy が「型ヒント必須」の方針に対して `disallow_untyped_defs` を持たず、型の無い関数が通る。 + - GitHub Actions のバージョンが固定されたまま古くなり(checkout@v5 / setup-python@v6 / setup-uv@v7)、更新する仕組みが無い。 + - bootstrap が `__init__.py` も build-system も生成しないため、生成直後のプロジェクトで `src/` 配下が import できず、CI が赤から始まる。 + +## 2. 決定 + +- 品質ゲートを次の 6 本にする: ruff format / ruff check / mypy / import-linter([ADR 0002](./0002-enforce-hexagonal-dependencies-with-import-linter.md))/ deptry / pytest(coverage 閾値つき)。 +- ruff の規則に `S`(flake8-bandit 相当)・`SIM`・`C4`・`RUF`・`PTH` を足す。日本語 docstring の全角括弧を誤検知する `RUF001`〜`RUF003` と、パッケージ / `__init__` の docstring を求める `D104` / `D107` は除外する。 +- mypy に `disallow_untyped_defs` / `disallow_incomplete_defs` / `strict_equality` を足す。テストにも同じ基準を適用する。src レイアウト用に `mypy_path = "src"` と `explicit_package_bases = true` を置く。 +- coverage を `branch = true` で計測し、`fail_under = 80` を CI の合否にする。 +- pre-commit に `pre-commit-hooks`(秘密鍵・巨大ファイル・TOML/YAML 構文・行末)と import-linter・deptry を足す。 +- GitHub Actions は `permissions: contents: read` と `concurrency` を置き、action を現行メジャーに更新する。以後の更新は Dependabot(`github-actions` + `uv`)に任せる。 +- build backend は uv 同梱の `uv_build` にする。 +- bootstrap は各層と `tests/` に `__init__.py`、smoke テスト、`.gitignore` の coverage 出力を生成し、生成直後に 6 本すべてが通る状態にする。 +- 優先した要件: 生成直後に CI が緑であること。ツールを増やしすぎないこと(bandit は ruff の `S` で代替)。ローカルと CI で同じコマンドを使うこと。 + +## 3. 代替案と不採用理由 + +- 代替案A: CI に `uv audit` または `pip-audit` を入れて脆弱性を止める。 + - 不採用理由: `uv audit` は uv 0.11 以降の experimental で、実行のたびに preview 警告が出て仕様が変わりうる。`pip-audit` は依存が 1 本増える。既知の脆弱性の通知は GitHub の Dependabot alerts(リポジトリ設定・CI 不要)で受ける。強化したい場合の手順は `tooling-best-practices.md` に残す。 +- 代替案B: mypy を `strict = true` にする。 + - 不採用理由: 既存の段階導入方針を維持する。「型ヒント必須」に直接対応する 3 つのフラグだけで目的を満たす。 +- 代替案C: bandit を別ツールとして入れる。 + - 不採用理由: ruff の `S` 規則で同じ検査ができる。 +- 代替案D: CI を lint / type / test の 3 ジョブに分けて並列化する。 + - 不採用理由: キャッシュが効けば `uv sync` は数秒で、YAML が 3 倍になる利得が無い。ステップ分割で失敗箇所は分かる。 +- 代替案E: `__init__.py` にパッケージ docstring を生成して `D104` を満たす。 + - 不採用理由: 定型文を bootstrap で保守することになる。パッケージの docstring は処理意図を書く場所ではない。 +- 代替案F: テストを mypy の override で緩める。 + - 不採用理由: 「型ヒント必須」に反する。`tests/` に `__init__.py` が無いと override 自体が効かない。テスト関数に `-> None` を書くだけで済む。 +- 代替案G: build backend に hatchling を使う。 + - 不採用理由: `uv_build` は uv 同梱で追加依存が無く、`uv init --package` の既定と一致する。 + +## 4. 影響 + +- コードへの影響: 既存プロジェクトに適用すると、型の無い関数・未使用依存・`S` 規則違反・カバレッジ不足で CI が落ちる。段階導入する場合は `ignore` と `fail_under` を一時的に緩め、理由を ADR に残す。 +- 運用への影響: 開発依存が 2 本増える(import-linter, deptry)。pre-commit の実行時間が伸びる(実測 14 秒程度、キャッシュ後)。Dependabot の PR が週 1 本届く。 +- ドキュメントへの影響: `python-uv-ci-setup` Playbook、`templates.md`、`tooling-best-practices.md`、bootstrap スクリプト、`.github/dependabot.yml`。 + +## 5. フォローアップ + +- [ ] テンプレートから立ち上げたプロジェクトの初回 PR で、GitHub Actions 上で 6 本が通ることを確認する。 +- [ ] `fail_under = 80` が実プロジェクトで妥当かを見直す。 +- [ ] `uv audit` が experimental を外れたら CI への追加を再検討する。 + +## 6. 変更履歴 + +- 2026-09-12: 初版作成。 diff --git a/docs/adr/README.md b/docs/adr/README.md index 72906eb..a9fa396 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -29,3 +29,5 @@ ## ADR 一覧 - [0001: ADR運用を導入して設計判断を記録する](./0001-record-architecture-decisions.md) - 承認済み(accepted) +- [0002: Hexagonal の依存方向を import-linter で機械検証する](./0002-enforce-hexagonal-dependencies-with-import-linter.md) - 承認済み(accepted) +- [0003: CI の品質ゲートを ruff / mypy / pytest の 3 本から 6 本へ拡充する](./0003-expand-ci-quality-gates.md) - 承認済み(accepted) diff --git a/docs/ai/canonical/playbooks/python-uv-ci-setup.md b/docs/ai/canonical/playbooks/python-uv-ci-setup.md index 3d50c54..df22e0b 100644 --- a/docs/ai/canonical/playbooks/python-uv-ci-setup.md +++ b/docs/ai/canonical/playbooks/python-uv-ci-setup.md @@ -1,39 +1,45 @@ --- name: python-uv-ci-setup -description: uv を使う Python プロジェクトで、format/lint/静的型チェック/テスト/docstring ルールをローカルと GitHub Actions で一貫運用するためのセットアップPlaybook。`pyproject.toml` の `[dependency-groups]`、`.pre-commit-config.yaml`、`.github/workflows/ci.yml` を新規作成または更新し、`uv run pre-commit install` まで完了させる依頼で使う。 +description: uv を使う Python プロジェクトで、format/lint/静的型チェック/依存方向の検証/依存の衛生/テスト/docstring ルールをローカルと GitHub Actions で一貫運用するためのセットアップPlaybook。`pyproject.toml` の `[dependency-groups]` と `[tool.importlinter]`、`.pre-commit-config.yaml`、`.github/workflows/ci.yml`、`.github/dependabot.yml` を新規作成または更新し、`uv run pre-commit install` まで完了させる依頼で使う。 --- # Python uv CIセットアップ -このPlaybookでは、`uv + ruff + mypy + pytest + pre-commit + GitHub Actions` を最小差分で導入し、ローカルとCIの品質ゲートをそろえる。 +このPlaybookでは、`uv + ruff + mypy + import-linter + deptry + pytest + pre-commit + GitHub Actions` を最小差分で導入し、ローカルとCIの品質ゲートをそろえる。 ## 実行フロー 1. 前提を確認する。 -- ルートに `pyproject.toml` があるか確認する。なければ `uv init` を提案する。 +- ルートに `pyproject.toml` があるか確認する。なければ `uv init --package --build-backend uv` を提案する。 +- `pyproject.toml` に `[build-system]` があるか確認する。src レイアウトでは必須(無いと `src/` 配下を mypy / import-linter / pytest が解決できない)。 - `uv --version` と `python --version` を確認する。 - Git管理下か確認する。未初期化なら `git init` を実行してから進む。 2. 既存設定を監査する。 -- `pyproject.toml` の `[dependency-groups]`、`[tool.ruff]`、`[tool.mypy]`、`[tool.pytest.*]` を確認する。 -- `.pre-commit-config.yaml` と `.github/workflows/*.yml` を確認する。 +- `pyproject.toml` の `[dependency-groups]`、`[tool.ruff]`、`[tool.mypy]`、`[tool.pytest.*]`、`[tool.coverage.*]`、`[tool.importlinter]`、`[tool.deptry]` を確認する。 +- `.pre-commit-config.yaml`、`.github/workflows/*.yml`、`.github/dependabot.yml` を確認する。 - 既存設定がある場合は上書きせず、重複を避けて統合する。 3. `pyproject.toml` を `uv` 前提で整備する。 - 開発依存を `dependency-groups.dev` に集約する。 -- 最低限の開発依存をそろえる: `ruff`, `mypy`, `pytest`, `pre-commit`。 +- 最低限の開発依存をそろえる: `ruff`, `mypy`, `import-linter`, `deptry`, `pytest`, `pytest-cov`, `pre-commit`。 - ルールは `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md` の `pyproject.toml` テンプレートを基準にし、既存プロジェクトに合わせて微調整する。 +- `[tool.importlinter]` の `your_project` を実際のパッケージ名に置換する。まだ無い層は `"(ports)"` のように括弧で囲んで省略可能にする。 +- `forbidden_modules` にプロジェクトで使う外部技術(Web フレームワーク、DB / HTTP クライアント)を足す。 4. pre-commit を設定する。 - `.pre-commit-config.yaml` を作成または更新する。 +- `pre-commit-hooks` の基本フック(秘密鍵・巨大ファイル・TOML/YAML 構文・行末)を入れる。 - `uv-pre-commit` の `uv-lock` を入れてロックファイル整合を強制する。 -- `uv run` 経由で `ruff format --check`、`ruff check`、`mypy` を実行する。 +- `uv run` 経由で `ruff format --check`、`ruff check`、`mypy`、`lint-imports`、`deptry` を実行する。 - `pytest` は既定で `pre-push` に配置して開発体験を維持する。全コミットで必須にしたい場合は `stages` を `pre-commit` に変更する。 -5. GitHub Actions を設定する。 +5. GitHub Actions と Dependabot を設定する。 - `.github/workflows/ci.yml` を作成または更新する。 -- `actions/setup-python` と `astral-sh/setup-uv` を使い、`uv sync --locked --dev` の後に同等チェックを実行する。 +- `permissions: contents: read` と `concurrency` を置く。 +- `actions/setup-python` と `astral-sh/setup-uv` を使い、`uv sync --locked --dev` の後にローカルと同じ 6 本のチェックを実行する。 - キャッシュは `setup-uv` の `enable-cache: true` を基本にする。 +- `.github/dependabot.yml` を作成し、`github-actions` と `uv` を週次更新にする。 6. ローカルセットアップを完了する。 - `uv lock` @@ -45,24 +51,33 @@ description: uv を使う Python プロジェクトで、format/lint/静的型 - `uv run ruff format --check .` - `uv run ruff check .` - `uv run mypy .` +- `uv run lint-imports` +- `uv run deptry src` - `uv run pytest -q` +- import-linter が層を見つけているか確かめる: `domain` から `adapters` を import する行をわざと 1 つ足し、`uv run lint-imports` が失敗することを確認してから戻す。契約が層を見つけられていないと静かに通ってしまうため、この確認を省略しない。 8. 結果を報告する。 - 追加・更新したファイル -- 実行コマンドと結果 -- 残課題(既存コード由来のlint/type/test失敗など) +- 実行コマンドと結果(逆依存で失敗した確認を含む) +- 残課題(既存コード由来のlint/type/test失敗、`fail_under` に届かないカバレッジなど) ## 運用ルール - 型チェックは `mypy` に固定し、`ty` は使わない。 +- 型ヒントは必須(`disallow_untyped_defs`)。テストにも同じ基準を適用し、override で緩めない。 +- 依存方向(`adapters -> application -> ports -> domain`)と `domain` / `ports` の外部技術への非依存は import-linter で検証する。文書の約束だけにしない。 +- import-linter の契約を緩める(`ignore_imports` を足す)ときは理由をコメントに残す。例外が増えるなら設計を見直す。 - docstring は Google style を採用し、短文 1 行のみの記述を避ける。 - docstring の先頭では「何をする処理か」「どの条件で使うか」を日本語で具体的に説明する。 - 引数がある処理は `Args`、戻り値がある処理は `Returns`、例外を送出しうる処理は `Raises` を記載する。 - `pydocstyle` の `convention = "google"` を有効化し、必要に応じて日本語運用に不要なルールのみ最小限で除外する。 - `project.requires-python` を定義し、Ruff のバージョン推論と整合させる。 +- カバレッジ閾値(`fail_under`)を下げるときは理由を ADR に残す。 - CI とローカルで実行コマンドを一致させる。 ## 参照ファイル - 設定方針と採用理由: `docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md` - そのまま適用できる雛形: `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md` +- 依存方向を機械検証する判断: `docs/adr/0002-enforce-hexagonal-dependencies-with-import-linter.md` +- 品質ゲート拡充の判断: `docs/adr/0003-expand-ci-quality-gates.md` diff --git a/docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md b/docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md index 2366475..e9f5ee1 100644 --- a/docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md +++ b/docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md @@ -1,14 +1,17 @@ -# テンプレート集(uv + ruff + mypy + pytest + pre-commit + GitHub Actions) +# テンプレート集(uv + ruff + mypy + import-linter + deptry + pytest + pre-commit + GitHub Actions) ## 目次 - [1. pyproject.toml(推奨テンプレート)](#1-pyprojecttoml推奨テンプレート) - [2. .pre-commit-config.yaml(推奨テンプレート)](#2-pre-commit-configyaml推奨テンプレート) - [3. .github/workflows/ci.yml(推奨テンプレート)](#3-githubworkflowsciyml推奨テンプレート) -- [4. セットアップ実行コマンド](#4-セットアップ実行コマンド) +- [4. .github/dependabot.yml(推奨テンプレート)](#4-githubdependabotyml推奨テンプレート) +- [5. セットアップ実行コマンド](#5-セットアップ実行コマンド) ## 1. pyproject.toml(推奨テンプレート) +`your-project` はプロジェクト名、`your_project` は `src/` 配下のパッケージ名に置換する。 + ```toml [project] name = "your-project" @@ -16,13 +19,19 @@ version = "0.1.0" requires-python = ">=3.12" dependencies = [] +[build-system] +requires = ["uv_build>=0.10.0,<0.13.0"] +build-backend = "uv_build" + [dependency-groups] dev = [ - "mypy>=1.11", - "pre-commit>=3.8", - "pytest>=8.3", - "pytest-cov>=5.0", - "ruff>=0.8", + "deptry>=0.25", + "import-linter>=2.15", + "mypy>=2.0", + "pre-commit>=4.0", + "pytest>=9.0", + "pytest-cov>=7.0", + "ruff>=0.16", ] [tool.ruff] @@ -32,37 +41,81 @@ line-length = 100 docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "I", "UP", "B", "D"] -ignore = ["D203", "D213", "D400", "D401", "D415"] +select = ["E4", "E7", "E9", "F", "I", "UP", "B", "D", "S", "SIM", "C4", "RUF", "PTH"] +ignore = [ + # パッケージ(__init__.py)と __init__ メソッドには docstring を求めない + "D104", "D107", + # Google style と日本語 docstring の運用に合わない規則 + "D203", "D213", "D400", "D401", "D415", + # 全角括弧などを「紛らわしい文字」として誤検知する(日本語 docstring 前提では除外) + "RUF001", "RUF002", "RUF003", +] [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = ["D100", "D101", "D102", "D103", "D104"] +"tests/**/*.py" = ["D100", "D101", "D102", "D103", "D104", "S101"] [tool.mypy] python_version = "3.12" +mypy_path = "src" +explicit_package_bases = true warn_unused_configs = true warn_return_any = true warn_unused_ignores = true -no_implicit_optional = true check_untyped_defs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +strict_equality = true show_error_codes = true pretty = true [tool.pytest.ini_options] minversion = "8.0" -addopts = "-ra --strict-markers --strict-config" +addopts = "-ra --strict-markers --strict-config --cov --cov-report=term-missing" testpaths = ["tests"] + +[tool.coverage.run] +branch = true +source = ["src"] + +[tool.coverage.report] +fail_under = 80 + +[tool.importlinter] +root_package = "your_project" +include_external_packages = true + +[[tool.importlinter.contracts]] +name = "Hexagonal layers: adapters -> application -> ports -> domain" +type = "layers" +containers = ["your_project"] +layers = ["adapters", "application", "ports", "domain"] + +[[tool.importlinter.contracts]] +name = "domain / ports do not depend on external technology" +type = "forbidden" +source_modules = ["your_project.domain", "your_project.ports"] +forbidden_modules = ["fastapi", "sqlalchemy", "httpx", "requests", "boto3"] ``` 適用メモ: +- `[build-system]` は必須。無いと `uv sync` がプロジェクト自身をインストールせず、`src/` 配下のパッケージを + mypy / import-linter / pytest が解決できない。`uv init --package --build-backend uv` が生成する値に合わせてよい。 - `requires-python` を更新したら `tool.mypy.python_version` も合わせる。 - Ruff は `project.requires-python` から推論可能なため、`target-version` は必要時のみ明示する。 - 既存プロジェクトで docstring 違反が多い場合は、`D` ルールを段階導入する。 - docstring は短文 1 行で終わらせず、概要と入出力が分かる情報を含める。 - `Args` / `Returns` / `Raises` は、該当する要素がある場合に必ず記載する。 +- `tool.mypy.mypy_path` と `explicit_package_bases` は src レイアウト用。無いと editable install と + `mypy .` の組み合わせで「同じファイルが 2 つのモジュール名で見つかる」エラーになる。 +- `[tool.importlinter]` の `layers` は上に書いた層ほど外側で、上から下へだけ import できる。 + まだ存在しない層は `"(ports)"` のように括弧で囲むと省略可能になる。 +- `forbidden_modules` はプロジェクトで使う外部技術(DB クライアント、HTTP クライアント、Web フレームワーク)に + 合わせて増やす。未インストールの名前を書いても動作する。 +- `fail_under` は初期値。プロジェクトの実態に合わせて上げる(下げるときは理由を ADR に残す)。 +- deptry は追加設定なしで動く。誤検知があるときだけ `[tool.deptry.per_rule_ignores]` で個別に除外する。 docstring 記載例(Google style): @@ -92,8 +145,18 @@ minimum_pre_commit_version: "3.7.0" default_install_hook_types: [pre-commit, pre-push] repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.10.0 + rev: 0.12.13 hooks: - id: uv-lock @@ -117,6 +180,18 @@ repos: language: system pass_filenames: false + - id: import-linter + name: import-linter + entry: uv run lint-imports + language: system + pass_filenames: false + + - id: deptry + name: deptry + entry: uv run deptry src + language: system + pass_filenames: false + - id: pytest name: pytest entry: uv run pytest -q @@ -128,6 +203,7 @@ repos: 適用メモ: - すべてのコミットで `pytest` を必須にする場合は `stages` を削除する。 - フックの実行対象を絞る場合は `files` を追加する。 +- `rev` は導入時点の最新タグ。以後の更新は `uv run pre-commit autoupdate` で行う。 ## 3. .github/workflows/ci.yml(推奨テンプレート) @@ -139,20 +215,27 @@ on: push: branches: [main] +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version-file: pyproject.toml - name: Set up uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v10 with: enable-cache: true @@ -168,6 +251,12 @@ jobs: - name: Mypy run: uv run mypy . + - name: Import Linter + run: uv run lint-imports + + - name: Deptry + run: uv run deptry src + - name: Pytest run: uv run pytest -q ``` @@ -175,11 +264,40 @@ jobs: 適用メモ: - `uv.lock` がない場合は先に `uv lock` を実行してコミットする。 - Python複数バージョン検証が必要なら `matrix` を追加する。 +- `permissions` は最小権限(読み取りのみ)。カバレッジのコメント投稿などで書き込みが要るジョブは、そのジョブにだけ権限を足す。 +- `concurrency` は同じブランチへの連続 push で古い実行を打ち切る。 + +## 4. .github/dependabot.yml(推奨テンプレート) + +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: ["*"] + + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + groups: + python: + patterns: ["*"] +``` + +適用メモ: +- `uv.lock` をコミットしていることが前提(`uv` エコシステムはロックファイルを更新する)。 +- 脆弱性の通知(Dependabot alerts)はこのファイルではなくリポジトリ設定で有効化する。 +- `groups` で週 1 本の PR にまとめている。個別 PR にしたい依存は `patterns` から外す。 -## 4. セットアップ実行コマンド +## 5. セットアップ実行コマンド ```bash -uv add --dev ruff mypy pytest pytest-cov pre-commit +uv add --dev ruff mypy import-linter deptry pytest pytest-cov pre-commit uv lock uv sync --locked --dev uv run pre-commit install --hook-type pre-commit --hook-type pre-push @@ -192,5 +310,7 @@ uv run pre-commit run --all-files uv run ruff format --check . uv run ruff check . uv run mypy . +uv run lint-imports +uv run deptry src uv run pytest -q ``` diff --git a/docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md b/docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md index 947ee59..b03b4c7 100644 --- a/docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md +++ b/docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md @@ -5,68 +5,142 @@ - `dependency-groups` を使い、開発依存は `dev` グループにまとめる。 - `uv sync` は既定で `dev` を同期するため、ローカル開発時の追加オプションを最小化できる。 - CI では `uv sync --locked --dev` を使い、`uv.lock` と整合する決定論的インストールに固定する。 +- src レイアウトでは `[build-system]` を必ず定義する(推奨は uv 同梱の `uv_build`)。 + 無いと `uv sync` がプロジェクト自身をインストールせず、`src/` 配下のパッケージを + mypy / import-linter / pytest が解決できない。 参考: - [uv dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/) +- [uv build backend](https://docs.astral.sh/uv/concepts/build-backend/) - [Using uv in GitHub Actions](https://docs.astral.sh/uv/guides/integration/github/) -## 2. Ruff(format/lint/docstring) +## 2. Ruff(format/lint/docstring/セキュリティ) - formatter と linter を Ruff に統一してツール数を減らす。 +- 基本セット(`E4/E7/E9/F/I/UP/B/D`)に次を足す。 + - `S`(flake8-bandit 相当): `eval` / `subprocess` の shell 実行 / 弱いハッシュなどセキュリティ上の問題。 + - `SIM`(flake8-simplify)と `C4`(comprehensions): 冗長な書き方の整理。 + - `RUF`(Ruff 固有): 可変既定値、未使用 `noqa` など。 + - `PTH`(flake8-use-pathlib): `os.path` より `pathlib` を使う。 +- bandit を別ツールとして入れない。`S` 規則で同じ検査ができる。 - `pydocstyle` は `convention = "google"` を指定する。 - docstring は短文 1 行のみで終わらせず、処理概要と利用条件が分かる説明を入れる。 - `Args` / `Returns` / `Raises` は、該当する要素がある場合に記載して入出力と失敗条件を明示する。 -- 日本語 docstring の運用では、英語前提になりやすいルール(例: `D400`, `D401`, `D415`)を必要に応じて除外する。 +- 日本語 docstring の運用では、英語前提になりやすいルール(例: `D400`, `D401`, `D415`)と、 + 全角括弧を「紛らわしい文字」として誤検知する `RUF001`〜`RUF003` を除外する。 +- パッケージ(`__init__.py`)と `__init__` メソッドの docstring(`D104`, `D107`)は求めない。 + 処理意図はモジュール・クラス・関数の docstring に書く。 +- テストでは `assert` を使うため `S101` を `tests/**` だけ除外する。 - `docstring-code-format = true` を有効化し、docstring 内コード例も整形対象にする。 参考: - [Ruff settings](https://docs.astral.sh/ruff/settings/) +- [Ruff rules](https://docs.astral.sh/ruff/rules/) ## 3. mypy(静的型チェック) - 型チェッカーは `mypy` に固定する。 -- 最初から `strict = true` を強制せず、`warn_unused_ignores` や `check_untyped_defs` などを段階的に有効化する。 +- 最初から `strict = true` を強制せず、必要なフラグを個別に有効化する。 +- 「型ヒント必須」の方針に直接対応するフラグを入れる。 + - `disallow_untyped_defs`: 型ヒントの無い関数定義をエラーにする。 + - `disallow_incomplete_defs`: 一部だけ型ヒントがある関数定義をエラーにする。 + - `strict_equality`: 型が重ならない値どうしの比較をエラーにする。 +- テストにも同じ基準を適用する(`tests.*` を override で緩めない)。テスト関数に `-> None` を書くだけで済む。 +- src レイアウトでは `mypy_path = "src"` と `explicit_package_bases = true` を設定する。 + 無いと editable install と `mypy .` の組み合わせで同じファイルが 2 つのモジュール名で見つかり、チェックが止まる。 - 出力可読性のため `show_error_codes = true` を有効化する。 - 設定は `pyproject.toml` に集約する。 参考: - [mypy config file](https://mypy.readthedocs.io/en/stable/config_file.html) +- [mypy: mapping file paths to modules](https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules) -## 4. pytest +## 4. import-linter(依存方向の検証) + +- Hexagonal Architecture の依存方向(`adapters -> application -> ports -> domain`)を `layers` 契約で検証する。 + 上に書いた層ほど外側で、上から下へだけ import できる。逆方向の import があると `lint-imports` が失敗する。 +- `domain` と `ports` が外部技術(Web フレームワーク、DB クライアント、HTTP クライアント)へ依存しないことを + `forbidden` 契約で検証する。`include_external_packages = true` にすると、未インストールのパッケージ名でも判定できる。 +- 契約は `pyproject.toml` の `[tool.importlinter]` に書き、実行は `uv run lint-imports`。 +- 層をまだ作っていないプロジェクトでは、`layers` の要素を `"(ports)"` のように括弧で囲んで省略可能にする。 +- 例外を許すときは契約の `ignore_imports` に `a.b -> c.d` の形で書き、理由をコメントに残す。 + 例外が増えるなら設計を見直す(契約を緩めるほうが先にならないようにする)。 +- 導入直後は、わざと逆方向の import を 1 つ足して失敗することを確かめてから戻す。 + 契約が層を見つけられていないときも静かに通ってしまうため、「失敗する」ことの確認が要る。 + +参考: +- [Import Linter](https://import-linter.readthedocs.io/) +- [Layers contract](https://import-linter.readthedocs.io/en/stable/contract_types.html#layers) + +## 5. deptry(依存の衛生) + +- `uv run deptry src` で、未使用の宣言依存・宣言されていない import・推移依存への直接 import を検出する。 +- `[project.dependencies]` と `[dependency-groups]` を自動で読むため、追加設定なしで動く。 +- 誤検知(プラグイン経由でしか import されないパッケージなど)は `[tool.deptry.per_rule_ignores]` で + 規則ごとに除外する。全体を無効化しない。 + +参考: +- [deptry](https://deptry.com/) + +## 6. pytest と coverage - 互換性重視で `[tool.pytest.ini_options]` を使う。 - `--strict-markers` と `--strict-config` を既定化し、設定ミスを早期検知する。 +- `--cov` を `addopts` に入れ、`[tool.coverage.run] branch = true` で分岐カバレッジを計測する。 +- `[tool.coverage.report] fail_under` で最低カバレッジを CI の合否にする。初期値は 80。 + 実行行が 0 のパッケージは 100% として扱われるため、生成直後のプロジェクトで詰まらない。 - 実行コマンドは `uv run pytest -q` を標準化する。 参考: - [pytest configuration](https://docs.pytest.org/en/stable/reference/customize.html) +- [coverage.py configuration](https://coverage.readthedocs.io/en/latest/config.html) -## 5. pre-commit +## 7. pre-commit +- `pre-commit-hooks` の基本フックを入れる。 + - `detect-private-key`: 秘密鍵のコミットを止める(秘密情報をコミットしない方針の機械的な網)。 + - `check-added-large-files`: 巨大ファイルの誤コミットを止める。 + - `check-toml` / `check-yaml`: 設定ファイルの構文を検査する。 + - `end-of-file-fixer` / `trailing-whitespace`: 行末の整形。 - `uv-pre-commit` の `uv-lock` を有効化し、依存追加時のロック更新漏れを防ぐ。 - ツール実行は `uv run` で統一し、ローカル/CIで同じ依存を使う。 - 実行時間を抑えるため、既定は以下を推奨する。 - - `pre-commit`: `ruff format --check`, `ruff check`, `mypy` + - `pre-commit`: `ruff format --check`, `ruff check`, `mypy`, `lint-imports`, `deptry` - `pre-push`: `pytest` - インストール時は hook type を明示する。 - `uv run pre-commit install --hook-type pre-commit --hook-type pre-push` 参考: - [pre-commit](https://pre-commit.com/) +- [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks) - [uv pre-commit integration](https://docs.astral.sh/uv/guides/integration/pre-commit/) -## 6. GitHub Actions +## 8. GitHub Actions - `actions/setup-python` と `astral-sh/setup-uv` を組み合わせる。 - `setup-uv` では `enable-cache: true` を使う。 -- `uv sync --locked --dev` の後、`ruff format --check`、`ruff check`、`mypy`、`pytest` を順番に実行する。 +- `permissions: contents: read` をワークフロー全体に置き、書き込みが要るジョブにだけ権限を足す。 +- `concurrency` で同じブランチの古い実行を打ち切る。 +- `uv sync --locked --dev` の後、`ruff format --check`、`ruff check`、`mypy`、`lint-imports`、`deptry`、`pytest` を順番に実行する。 - 失敗時に原因を分離しやすいよう、ステップを分割する。 +- action のメジャーバージョンは導入時点の最新を使い、以後の更新は Dependabot に任せる。 参考: - [setup-uv action](https://github.com/astral-sh/setup-uv) - [Using uv in GitHub Actions](https://docs.astral.sh/uv/guides/integration/github/) +- [GITHUB_TOKEN permissions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication) + +## 9. Dependabot + +- `.github/dependabot.yml` で `github-actions` と `uv` の 2 エコシステムを週次更新にする。 +- `groups` でまとめ、週 1 本の PR にする。 +- 脆弱性の通知(Dependabot alerts)と自動修正 PR(security updates)はリポジトリ設定で有効化する。 + CI に脆弱性スキャンを入れなくても、既知の脆弱性は GitHub 側から通知される。 + +参考: +- [Dependabot configuration options](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference) -## 7. 推奨実行順序 +## 10. 推奨実行順序 1. `uv lock` 2. `uv sync --locked --dev` @@ -74,11 +148,13 @@ 4. `uv run pre-commit run --all-files` 5. `uv run pytest -q` -## 8. 改善案(標準より強化する場合) +## 11. 改善案(標準より強化する場合) - 型安全性を強化する場合: - - `mypy` で `disallow_untyped_defs = true` を段階導入する。 + - `mypy` で `strict = true` へ段階的に移行する。 - CI速度を改善する場合: - ジョブ分割(lint/type/test)を行い並列化する。 -- テスト品質を強化する場合: - - `pytest-cov` を導入し、最低カバレッジ閾値を設定する。 +- 脆弱性を CI でも止めたい場合: + - `uv audit`(uv 0.11 以降。experimental で仕様が変わりうる)か `pip-audit` をステップに足す。 +- ライブラリとして配布する場合: + - `matrix` で複数の Python バージョンを検証する。 diff --git a/docs/ai/playbooks/python-uv-ci-setup.md b/docs/ai/playbooks/python-uv-ci-setup.md index d78a48a..7175b75 100644 --- a/docs/ai/playbooks/python-uv-ci-setup.md +++ b/docs/ai/playbooks/python-uv-ci-setup.md @@ -1,6 +1,6 @@ --- name: python-uv-ci-setup -description: uv を使う Python プロジェクトで、format/lint/静的型チェック/テスト/docstring ルールをローカルと GitHub Actions で一貫運用するためのセットアップPlaybook。`pyproject.toml` の `[dependency-groups]`、`.pre-commit-config.yaml`、`.github/workflows/ci.yml` を新規作成または更新し、`uv run pre-commit install` まで完了させる依頼で使う。 +description: uv を使う Python プロジェクトで、format/lint/静的型チェック/依存方向の検証/依存の衛生/テスト/docstring ルールをローカルと GitHub Actions で一貫運用するためのセットアップPlaybook。`pyproject.toml` の `[dependency-groups]` と `[tool.importlinter]`、`.pre-commit-config.yaml`、`.github/workflows/ci.yml`、`.github/dependabot.yml` を新規作成または更新し、`uv run pre-commit install` まで完了させる依頼で使う。 --- @@ -8,35 +8,41 @@ description: uv を使う Python プロジェクトで、format/lint/静的型 # Python uv CIセットアップ -このPlaybookでは、`uv + ruff + mypy + pytest + pre-commit + GitHub Actions` を最小差分で導入し、ローカルとCIの品質ゲートをそろえる。 +このPlaybookでは、`uv + ruff + mypy + import-linter + deptry + pytest + pre-commit + GitHub Actions` を最小差分で導入し、ローカルとCIの品質ゲートをそろえる。 ## 実行フロー 1. 前提を確認する。 -- ルートに `pyproject.toml` があるか確認する。なければ `uv init` を提案する。 +- ルートに `pyproject.toml` があるか確認する。なければ `uv init --package --build-backend uv` を提案する。 +- `pyproject.toml` に `[build-system]` があるか確認する。src レイアウトでは必須(無いと `src/` 配下を mypy / import-linter / pytest が解決できない)。 - `uv --version` と `python --version` を確認する。 - Git管理下か確認する。未初期化なら `git init` を実行してから進む。 2. 既存設定を監査する。 -- `pyproject.toml` の `[dependency-groups]`、`[tool.ruff]`、`[tool.mypy]`、`[tool.pytest.*]` を確認する。 -- `.pre-commit-config.yaml` と `.github/workflows/*.yml` を確認する。 +- `pyproject.toml` の `[dependency-groups]`、`[tool.ruff]`、`[tool.mypy]`、`[tool.pytest.*]`、`[tool.coverage.*]`、`[tool.importlinter]`、`[tool.deptry]` を確認する。 +- `.pre-commit-config.yaml`、`.github/workflows/*.yml`、`.github/dependabot.yml` を確認する。 - 既存設定がある場合は上書きせず、重複を避けて統合する。 3. `pyproject.toml` を `uv` 前提で整備する。 - 開発依存を `dependency-groups.dev` に集約する。 -- 最低限の開発依存をそろえる: `ruff`, `mypy`, `pytest`, `pre-commit`。 +- 最低限の開発依存をそろえる: `ruff`, `mypy`, `import-linter`, `deptry`, `pytest`, `pytest-cov`, `pre-commit`。 - ルールは `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md` の `pyproject.toml` テンプレートを基準にし、既存プロジェクトに合わせて微調整する。 +- `[tool.importlinter]` の `your_project` を実際のパッケージ名に置換する。まだ無い層は `"(ports)"` のように括弧で囲んで省略可能にする。 +- `forbidden_modules` にプロジェクトで使う外部技術(Web フレームワーク、DB / HTTP クライアント)を足す。 4. pre-commit を設定する。 - `.pre-commit-config.yaml` を作成または更新する。 +- `pre-commit-hooks` の基本フック(秘密鍵・巨大ファイル・TOML/YAML 構文・行末)を入れる。 - `uv-pre-commit` の `uv-lock` を入れてロックファイル整合を強制する。 -- `uv run` 経由で `ruff format --check`、`ruff check`、`mypy` を実行する。 +- `uv run` 経由で `ruff format --check`、`ruff check`、`mypy`、`lint-imports`、`deptry` を実行する。 - `pytest` は既定で `pre-push` に配置して開発体験を維持する。全コミットで必須にしたい場合は `stages` を `pre-commit` に変更する。 -5. GitHub Actions を設定する。 +5. GitHub Actions と Dependabot を設定する。 - `.github/workflows/ci.yml` を作成または更新する。 -- `actions/setup-python` と `astral-sh/setup-uv` を使い、`uv sync --locked --dev` の後に同等チェックを実行する。 +- `permissions: contents: read` と `concurrency` を置く。 +- `actions/setup-python` と `astral-sh/setup-uv` を使い、`uv sync --locked --dev` の後にローカルと同じ 6 本のチェックを実行する。 - キャッシュは `setup-uv` の `enable-cache: true` を基本にする。 +- `.github/dependabot.yml` を作成し、`github-actions` と `uv` を週次更新にする。 6. ローカルセットアップを完了する。 - `uv lock` @@ -48,24 +54,33 @@ description: uv を使う Python プロジェクトで、format/lint/静的型 - `uv run ruff format --check .` - `uv run ruff check .` - `uv run mypy .` +- `uv run lint-imports` +- `uv run deptry src` - `uv run pytest -q` +- import-linter が層を見つけているか確かめる: `domain` から `adapters` を import する行をわざと 1 つ足し、`uv run lint-imports` が失敗することを確認してから戻す。契約が層を見つけられていないと静かに通ってしまうため、この確認を省略しない。 8. 結果を報告する。 - 追加・更新したファイル -- 実行コマンドと結果 -- 残課題(既存コード由来のlint/type/test失敗など) +- 実行コマンドと結果(逆依存で失敗した確認を含む) +- 残課題(既存コード由来のlint/type/test失敗、`fail_under` に届かないカバレッジなど) ## 運用ルール - 型チェックは `mypy` に固定し、`ty` は使わない。 +- 型ヒントは必須(`disallow_untyped_defs`)。テストにも同じ基準を適用し、override で緩めない。 +- 依存方向(`adapters -> application -> ports -> domain`)と `domain` / `ports` の外部技術への非依存は import-linter で検証する。文書の約束だけにしない。 +- import-linter の契約を緩める(`ignore_imports` を足す)ときは理由をコメントに残す。例外が増えるなら設計を見直す。 - docstring は Google style を採用し、短文 1 行のみの記述を避ける。 - docstring の先頭では「何をする処理か」「どの条件で使うか」を日本語で具体的に説明する。 - 引数がある処理は `Args`、戻り値がある処理は `Returns`、例外を送出しうる処理は `Raises` を記載する。 - `pydocstyle` の `convention = "google"` を有効化し、必要に応じて日本語運用に不要なルールのみ最小限で除外する。 - `project.requires-python` を定義し、Ruff のバージョン推論と整合させる。 +- カバレッジ閾値(`fail_under`)を下げるときは理由を ADR に残す。 - CI とローカルで実行コマンドを一致させる。 ## 参照ファイル - 設定方針と採用理由: `docs/ai/playbook-assets/python-uv-ci-setup/references/tooling-best-practices.md` - そのまま適用できる雛形: `docs/ai/playbook-assets/python-uv-ci-setup/references/templates.md` +- 依存方向を機械検証する判断: `docs/adr/0002-enforce-hexagonal-dependencies-with-import-linter.md` +- 品質ゲート拡充の判断: `docs/adr/0003-expand-ci-quality-gates.md` diff --git a/scripts/playbooks/python-project-bootstrap/bootstrap_python_project.py b/scripts/playbooks/python-project-bootstrap/bootstrap_python_project.py index 805e40e..0b7c593 100755 --- a/scripts/playbooks/python-project-bootstrap/bootstrap_python_project.py +++ b/scripts/playbooks/python-project-bootstrap/bootstrap_python_project.py @@ -101,6 +101,9 @@ def ensure_gitignore(target: Path, report: dict[str, list[str]]) -> None: ".pytest_cache/", ".mypy_cache/", ".ruff_cache/", + ".coverage", + "htmlcov/", + "coverage.xml", ".worktrees/", ] gitignore_path = target / ".gitignore" @@ -298,10 +301,10 @@ def build_code_architecture_rules(package_name: str) -> str: ## 依存方向 -- `src/{package_name}/adapters` -> `src/{package_name}/application` -- `src/{package_name}/application` -> `src/{package_name}/domain` +- `adapters` -> `application` -> `ports` -> `domain` の順に外側から内側へだけ import できる。 - `src/{package_name}/ports` は契約のみを保持し、実装は持たない。 -- `domain` は外部ライブラリ・フレームワークへ依存しない。 +- `domain` と `ports` は外部ライブラリ・フレームワークへ依存しない。 +- 依存方向は import-linter(`uv run lint-imports`)で検証する。契約は `pyproject.toml` の `[tool.importlinter]` にある。 ## 層責務 @@ -714,6 +717,28 @@ def build_api_endpoint_template() -> str: return load_template(API_ENDPOINT_TEMPLATE_PATH, fallback) +def build_smoke_test(package_name: str) -> str: + """パッケージが import できることを確かめる smoke テストを返す。 + + 生成直後のプロジェクトでも pytest が「テスト 0 件」で失敗せず、 + `src/` 配下のパッケージが解決できることを CI で確認できるようにする。 + + Args: + package_name: `src/` 配下のパッケージ名。 + + Returns: + `tests/unit/test_smoke.py` の内容。 + """ + return f'''"""パッケージが import できることを確かめる smoke テスト。""" + +import importlib + + +def test_package_is_importable() -> None: + assert importlib.import_module("{package_name}") +''' + + def build_env_file(environment: str) -> str: """環境別 .env テンプレートを返す。""" suffix = environment.upper() @@ -789,7 +814,19 @@ def main() -> None: ] ensure_directories(directories, report) + # src/ と tests/ の配下は全部 Python パッケージにする(__init__.py を置く)。 + # 無いと import-linter が層を見つけられず、mypy / pytest も src 配下を解決できない。 + package_roots = (target / "src" / package_name, target / "tests") + package_dirs: set[Path] = set(package_roots) + for path in directories: + for root in package_roots: + if path.is_relative_to(root): + package_dirs.update(p for p in [path, *path.parents] if p.is_relative_to(root)) + init_files = {path / "__init__.py": "" for path in sorted(package_dirs)} + files = { + **init_files, + target / "tests" / "unit" / "test_smoke.py": build_smoke_test(package_name), target / "AGENTS.md": build_agents_md( project_name=args.project_name, description=args.description,