python-monorepo-architecture について
uv ワークスペースで管理する複数パッケージの Python リポジトリを設計するスキルです。パッケージ境界、内向きの依存関係、公開 API、共有テスト、コード抽出を支援します。
主な機能
- 01パッケージ分割の要否を判断
- 02uv ワークスペースのメンバーと依存関係を設定
- 03循環せず共有ライブラリへ向かう依存関係を維持
- 04公開パッケージ API とインポート境界を定義
- 05メンバーごとのテストと共有フィクスチャを整理
- 06回帰チェックを伴う共有コードの抽出
活用シーン
- 01複数アプリで使う共有ライブラリの設計
- 02循環インポートやアプリ間インポートの診断
- 03アプリパッケージから再利用コードを移動
- 04ワークスペースの依存関係とテスト規約の策定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bm629/ agent-skills --skill python-monorepo-architectureインストール後、Claude Code で/python-monorepo-architectureを入力して読み込まれたか確認します。
同梱ファイルreferences/sources.mdreferences/worked-example.md
詳細
python-monorepo-architecture — SKILL.md
バリアント: standard · 使用するタイミング: 複数パッケージの Python uv ワークスペース・モノレポにおける、パッケージ間の構成を設計するとき。
概要
このスキルでは、Python モノレポのパッケージ間のレイヤーを扱います。uv ワークスペースで管理される、複数のインストール可能なパッケージ(メンバー)を含む単一のリポジトリが対象です。通常は、共有ライブラリと、それを基盤とする 1 つ以上のアプリケーションまたは CLI メンバーで構成されます。パッケージ間に関わる判断、つまりライブラリとアプリケーションの境界をどこに設けるか、依存関係をどちら向きに許可するか、ツールで強制できない場合に境界をどう守るか、各メンバーが他のメンバーに何を公開するか、共有テストをどこに置くか、既存パッケージを壊さずに共有ライブラリを切り出す方法を取り上げます。パッケージ内のモジュール構成(__all__、レイヤー構造。これは python-project-structure スキルで扱います)や、uv の一般的な使い方(uv add/uv run。これは uv スキルで扱います)を改めて説明するものではありません。これらのスキルでは扱わないため、uv ワークスペースの配線はこのスキルの対象とし、両方のスキルと組み合わせて使用します。
有効化するタイミング
- ✅ 成長したコードベースを共有ライブラリの
libs/<core>とapps/<app>/tools/<cli>メンバーに分割するか、1 つのパッケージのままにするかを決めるとき。 - ✅ あるメンバーが別のメンバー(共有ライブラリ)に依存する uv ワークスペースを構成するとき。
- ✅ パッケージ間の循環インポートや複雑に絡み合った依存関係を診断、または防止するとき。
- ✅ アプリケーションや CLI パッケージから共有ロジックを新しい共有ライブラリに切り出すとき。
- ✅ パッケージ境界で各メンバーが公開する API と、利用側からのインポート方法を決めるとき。
次の場合は有効化しないでください:
- 作業が単一パッケージ内のもの(モジュール構成、
__all__、パッケージの__init__によるエクスポート)の場合 →python-project-structureを使用してください。 - 作業が uv の一般的な使い方(依存関係の追加、スクリプトの実行、ロックファイル)の場合 →
uvスキルを使用してください。 - リポジトリにデプロイ対象が 1 つだけあり、切り出す共有コードもない場合は、単一パッケージが適切です。分割するのは時期尚早です(「アンチパターン」を参照してください)。
- JS/pnpm または Cargo のワークスペースの場合。このスキルは Python と uv に特化しています。
ワークフロー
ステップ 1:そもそも分割するかどうかを決める
複数メンバーに分割するのは、2 つ以上のデプロイ対象で実際に共有されるコードがある場合、または独立してテストできるコアによってデプロイ対象にメリットがある場合に限ります。分割を検討する兆候:2 つのアプリケーション(例:CLI と Web サービス)が同じドメインロジックを必要としている、または 1 つのアプリケーションが再利用可能なロジックの薄いラッパーになっている。分割を検討すべきでない兆候:
- デプロイ対象が 1 つで、再利用もない場合 — 1 つのパッケージにまとめてください。
- メンバー間で競合する依存関係のバージョンや個別の仮想環境が必要な場合 — uv ワークスペースでは環境とロックファイルを共有するため、この場合はワークスペースのメンバーではなくパス依存関係を使用するか(ステップ 2 を参照)、リポジトリを分けてください。
- メンバーごとに異なる
requires-pythonが必要な場合 — uv ワークスペースでは、すべてのメンバーに共通する単一のrequires-pythonが適用されるため、異なる Python の最低バージョンは 1 つのワークスペースに適しません。
分割する場合の一般的な構成は次のとおりです。
repo/
pyproject.toml # workspace root: [tool.uv.workspace] members
libs/core/ # shared library member (package: core)
apps/api/ # web service member (package: api) -> depends on core
apps/cli/ # CLI member (package: cli) -> depends on core
ステップ 2:uv ワークスペースを設定する(このスキルの担当範囲)
ルートの pyproject.toml で、メンバーを glob として宣言します。
[tool.uv.workspace]
members = ["libs/*", "apps/*"]
exclude = ["libs/scratch"] # optional
members に一致し、exclude されていないすべてのディレクトリには、それぞれ独自の pyproject.toml が必要です。メンバーが別のメンバーに依存する場合は、通常の依存関係として名前を指定し、さらにソースをワークスペースとして指定します。
# apps/api/pyproject.toml
[project]
dependencies = ["core"]
[tool.uv.sources]
core = { workspace = true } # resolve `core` from the workspace, not PyPI; editable
主な仕組み:
- ワークスペース全体で 共有の
uv.lockと単一の仮想環境を使用します。uv lockはワークスペース全体を対象に動作します。uv run/uv syncはデフォルトでルートを対象とし、特定のメンバーを対象にするには--package <member>を指定します。 - ワークスペースの依存関係は 編集可能 です。
coreを編集すると、再インストールしなくても変更がすぐにapiから見えるようになります。 - ルートの
[tool.uv.sources]はすべてのメンバーに適用されます。メンバー独自の[tool.uv.sources]がある場合は、ルートの設定を上書きします。 - 代替策: メンバー間で環境を共有できない場合は、パス依存関係(
core = { path = "../../libs/core" })を使用するか、別々に分けます。ワークスペースにはしません。
(uv の基本的な使い方である uv add / uv run / ロックファイルについては uv スキルを、メンバーの内部モジュール構成については python-project-structure を参照してください。)
ステップ 3:依存関係の方向を正す — 内側に依存させ、非循環に保つ
パッケージ依存関係のグラフは DAG であり、共有コアに向かう内向きの依存関係でなければなりません。
- アプリ/CLI は共有ライブラリに依存します:
api → core、cli → core。 - アプリ同士が依存することはありません:
api → cli、cli → apiは不可です。 - 共有ライブラリはどちらのアプリにも依存しません。
coreにはアプリへの依存関係がありません。アプリにある何かを「必要とする」共有コードは、その機能をcore(または別のライブラリ)に置くべきだという兆候です。coreが上位にあるアプリをインポートすべきだという意味ではありません。
アプリ間のインポートや上位へのインポートはアンチパターンです。直接的または推移的に循環を生み、独立してリリースすべきデプロイ可能なアプリ同士を密結合させます。2 つのアプリで新たに共有するものがある場合は、横方向にインポートせず、core(または 2 つ目のライブラリ)に下ろしてください。
ステップ 4:境界を形だけのものにしない — uv では強制できない
uv ワークスペースでは 1 つの環境を共有し、Python には依存関係の分離機能がありません。実行時に、あるメンバーが兄弟メンバーの宣言したパッケージ(またはモジュール)をインポートするのを止めるものはありません。uv 自身も、「ワークスペース内の別のメンバーが宣言した依存関係をパッケージがインポートしないことは保証できない」と述べています。そのため、この境界はツールではなく、規約とコードレビューによって守ります。
-
横方向または上位へのインポート(ステップ 3)は、レビューでブロックすべき欠陥として扱います。
-
メンバーが実際に使用する依存関係は、すべてそのメンバーの
pyproject.tomlで宣言します。兄弟メンバーを経由した推移的なインストールには決して頼りません。 -
任意の自動チェック —
import-linter。 CI または pre-commit フックで.importlinterの契約をチェックし、禁止されたインポートがあればビルドを失敗させます。これらのルールを表現する契約タイプは次の 2 つです。- Independence — 「これらのモジュールは互いをインポートしない」→ アプリ同士が相互に独立していること(
api ↔ cliがないこと)を保証します。 - Layers / Forbidden — 層の順序(
coreより上位にアプリを配置する)や、「coreにアプリのインポートを禁止する」ことを表現し、内向きの依存関係を強制します。
これは最低限のセーフティネットとして使います。上記のルールは、これを使うかどうかにかかわらず守ってください。
- Independence — 「これらのモジュールは互いをインポートしない」→ アプリ同士が相互に独立していること(
ステップ 5:各メンバーの境界で公開 API を定義する
利用側はメンバーの公開インターフェース(パッケージの __init__ / __all__)を通じてメンバーをインポートし、それ以外はすべて内部実装として扱います。したがって、次のようにします。
- 共有ライブラリが意図する API を
libs/core/src/core/__init__.pyから(__all__を使って)エクスポートします。利用側ではfrom core import Xとします。 - アプリは
coreの_internalモジュールではなく、その境界に依存します。別のメンバーのプライベートモジュールに踏み込むと、内部実装への結びつきが再び強まり、分割の目的が損なわれます。 - (パッケージ内の
__all__/ アンダースコアによる非公開化の仕組みはpython-project-structureスキルの範囲です。このスキルでは、メンバー間のルール、つまり「内部実装ではなく境界を利用する」ことだけを追加します。)
各メンバーで src レイアウト(libs/core/src/core/、apps/api/src/api/)を使い、テストが誤って最上位からインポートしたモジュールではなく、インストール済みパッケージを対象に実行されるようにします。これはメンバー間で一貫させます。
ステップ 6:テストと共有フィクスチャを配置する
- 各メンバーは、それぞれの
conftest.pyを含む独自のtests/を持ちます。pytest はディレクトリごとにフィクスチャを自動検出します。 - 本当にメンバー間で共有されるフィクスチャには、コピー&ペーストした
conftestではなく、pytest_pluginsを介して読み込む小規模な共有テストサポートパッケージ(独自のメンバー、またはtests/fixtures/モジュール)などを優先します。フィクスチャは、それが表すもののできるだけ近くに配置します。リポジトリ全体で使うフィクスチャだけを最上位に置きます。 - メンバー間で
tests/ディレクトリを直接参照しないでください。共有テストサポートパッケージを通じて共有します。
ステップ 7:共有コードを安全に切り出す(既存パッケージを分割する場合)
既存アプリからライブラリを切り出すのは、動作を維持するリファクタリングです。まずセーフティネットを整えます。
- まず既存のテストスイートがすべて成功することを確認します(それがセーフティネットになります)。
- 新しい
libs/<core>メンバーを作成し(ステップ 2)、アプリがそれに依存するよう設定します。 - 共有モジュールを
coreに移動します(書き換えず、移動します)。アプリのインポート先をfrom core import …に変更します。 - 既存のテストスイート全体を実行します。テスト件数が同じまま、すべて成功しなければなりません。移動したモジュールの単体テストを
libs/core/tests/に移します。 - 移動によって循環依存が明らかになった場合(移動したコードがアプリを再びインポートしている、またはライブラリも必要とするものをアプリが必要としている場合)は、共有依存を
coreにも移動するか、依存関係を逆転させて解消します(アプリ固有の要素をパラメーターまたはインターフェース経由で渡します)。上位へのインポートで解決してはいけません。
3 メンバー(ルート + ライブラリ + 2 つのアプリ)の手順全体、.importlinter コントラクト、および段階的な切り出しについては、references/worked-example.md を参照してください。
ルール
必須ルール(決して違反しないこと):
- パッケージ間の依存関係グラフは、内側を向く DAG です。アプリ → 共有ライブラリとし、アプリ同士は依存させず、共有ライブラリはアプリに依存しません。
- 各メンバーは自身の依存関係を宣言します。兄弟メンバーの推移的なインストールに頼ってはいけません。
- 利用側は、別のメンバーの
_internalモジュールではなく、その公開 API(__init__/__all__)を通じてインポートします。 - メンバー間の依存には
[tool.uv.sources] { <name> = { workspace = true } }を使います。メンバー間で単一の環境やrequires-pythonを共有できない場合、それはワークスペースではありません(パス依存または別々のリポジトリを使います)。 - 切り出しではまずセーフティネットを整えます。既存テストスイートが成功 → 移動 → インポート先を変更 → テストスイートが引き続き成功、という順序にします。循環依存が明らかになった場合は、上位へのインポートではなく、コードを下位に移して解消します。
推奨事項(必要に応じて変更可):
- インストールに基づくテスト検出を一貫させるため、各メンバーで src レイアウトを使います。
- アプリが複数になったら、CI で境界を強制するために
import-linterコントラクト(Independence + Layers/Forbidden)を追加します。 - フィクスチャは、重複した
conftestではなく、テストサポートパッケージとpytest_pluginsを通じて共有します。 - メンバーの数は少なく保ちます。実際にデプロイするもの、または本当に再利用するものがある場合にのみ、メンバーを追加します。
注意点
- uvでは不正なインポートを止められません。 ワークスペースでは1つの venv を共有するため、
api内のfrom cli import …はアーキテクチャに違反していても、実行時には動作します。uvではこれを検出できません。検出できるのはレビュー(またはimport-linter)だけです。「インポートできるから許可されている」と考えないでください。 - ワークスペース全体で
requires-pythonは1つです。 uvは各メンバーの条件の共通部分を使用します。より高い Python バージョンの下限を必要とするメンバーがあると、全メンバーの下限が暗黙に引き上げられるか(またはそのメンバーがワークスペースに適合しなくなります)。Python バージョンの下限はワークスペースレベルで決めてください。 - 兄弟メンバーの内部に踏み込むこと。
from core._internal.thing import xはコンパイルされ、実行もできますが、予告なく変更される可能性のあるプライベートな構造に再び依存することになります。インポートするのは境界のみとしてください。 - 早すぎる分割。 実際に再利用されていないコードを2メンバーのワークスペースに分割しても、ロックファイルや配線、境界に関するオーバーヘッドが増えるだけです。推測で分割するのではなく、実際に再利用が必要になったとき、または独立してテストできるコアができたときに分割してください。
- 「成功」しているのに誤っている抽出。 モジュールの移動後、テストスイートが引き続き成功していても、テストを通すために追加した新たな 上向き のインポートが隠れていることがあります。移動後はテスト結果だけでなく、依存関係の方向を明示的に確認してください。
[tool.uv.sources]の記述漏れ。dependenciesにcoreを記載しても、{ workspace = true }のソースを指定しなければ、uvは PyPI から解決しようとします。その結果、解決に失敗するか、無関係な人のパッケージを取得してしまいます。ワークスペース依存関係として機能させるのは、このソース行です。
アンチパターン
- 「別のアプリのヘルパーをインポートすればいいだけ。」 1つの関数を再利用するために、横向きの
app → appインポートを行うことです。代わりにヘルパーを共有ライブラリへ下ろしてください。横向きのインポートは、デプロイ可能なアプリ同士を循環依存させ、結合させます。 - 「今回だけはライブラリからアプリをインポートしてもいい。」 上向きの(lib → app)インポートは、どれもDAGを逆転させます。ライブラリが必要とするものはライブラリ内に置くか、渡すようにしてください。上向きに取得することは決してありません。
- 「どうせすべて同じ venv だから、境界は関係ない。」 共有環境は利便性のためのものであり、境界を越える許可ではありません。だからこそ、規律とレビューが必要です。
- 「すべてのモジュールを個別のパッケージに分割しよう。」 過剰な細分化です。モジュールごとにパッケージを作ると、pyprojectや境界に関するオーバーヘッドが何倍にもなります。メンバーにするのは、デプロイ可能なもの、または真に共有されるライブラリであり、すべてのフォルダーではありません。
- 「公開 API を広げないために
_internalに踏み込もう。」 利用側が必要とするものなら、意図的に境界上で公開してください。プライベートなもののインポートは、隠れた結合です。
成果物
成果物は判断と構造です。どのパッケージを、なぜ作るのかを示すメンバー構成、ルートと各メンバーの pyproject.toml の配線([tool.uv.workspace] と [tool.uv.sources])、非循環で内向きに依存する方向の明示、各メンバーの公開 API 境界、テストとフィクスチャの配置、そしてリファクタリング時には安全策を先に整える抽出手順が含まれます。利用者は、ワークスペースを構築または再編成するエンジニアやコーディングエージェントです。成果物となる構造を基盤に、その後の機能開発が進められます。
関連項目
uv— uvの一般的な使用方法(uv add、uv run、ロックファイル)。このスキルが扱うのは、uvスキルではカバーしないワークスペースの配線です。ワークスペース以外のことにはuvを使用してください。python-project-structure— パッケージ内のモジュールアーキテクチャ、__all__、階層構造。このスキルが扱うのは、その上位にあるパッケージ間のレイヤーです。両方を併用してください(このスキルではメンバーの境界を、もう一方ではメンバー内の構造を扱います)。import-linter(外部ツール)—.importlinterコントラクトを使って境界を任意で CI により適用します。
段階的な情報開示
references/worked-example.md— 3メンバーのワークスペースを詳しく解説します(ルート、共有ライブラリ、2つのアプリとpyproject.tomlの依存関係)。.importlinterコントラクト(Independence + Layers)の例と、安全な抽出を段階的に進める手順も含まれます。具体的なワークスペースを構築または再編成するときに読み込んでください。references/sources.md— 調査の出典/引用。
本文の上限
descriptionは 1,024 文字以内です。Claude は一覧表示でdescriptionとwhen_to_useの合計が 1,536 文字を超えると切り詰めます。- 本文は約 500 行/5,000 トークン以内です。
- 参照ファイルごとに、10k トークンを超えると警告、25k トークンを超えるとエラーになります。参照ファイル全体では、25k トークンを超えると警告、50k トークンを超えるとエラーになります。