关于 python-monorepo-architecture
用于规划 uv 工作区多包 Python 仓库结构的技能,帮助团队定义包边界、单向依赖、公开 API、共享测试,并安全地提取可复用代码。
主要功能
- 01判断是否应拆分为多个包
- 02配置 uv 工作区成员及依赖
- 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
**变体:**标准 · **适用场景:**设计包含多个 Python 包的 uv 工作区单体仓库的跨包结构。
概述
此技能介绍 Python 单体仓库的跨包层:一个仓库包含多个可安装包(成员),由 uv 工作区管理——通常是一个共享库,以及一个或多个基于该库构建的应用或 CLI 成员。它涵盖包与包之间的设计决策:库与应用的边界在哪里、依赖可以指向哪个方向、工具无法强制时如何确保边界不被破坏、各成员向其他成员公开什么内容、共享测试放在哪里,以及如何从现有包中拆分出共享库而不影响原有功能。它不会重新讲解包内模块布局(__all__、分层结构——请参阅 python-project-structure 技能)或 uv 的常规用法(uv add/uv run——请参阅 uv 技能);它负责配置 uv 工作区,因为其他技能不涉及这一内容,并且与这两个技能配合使用。
何时启用
- ✅ 决定是否将不断增长的代码库拆分为共享的
libs/<core>加apps/<app>/tools/<cli>成员,或继续保留为一个包。 - ✅ 配置 uv 工作区,让一个成员依赖另一个成员(共享库)。
- ✅ 诊断或预防包之间的循环导入/依赖纠缠。
- ✅ 将应用或 CLI 包中的共享逻辑提取到新的共享库中。
- ✅ 决定成员在包边界处应公开的 API,以及使用者应如何导入。
不要在以下情况启用:
- 工作内容在一个包内部——模块布局、
__all__、包__init__导出 → 使用python-project-structure。 - 工作内容是 uv 的常规用法——添加依赖项、运行脚本、处理锁文件 → 使用
uv技能。 - 仓库只有一个可部署项目,且没有需要提取的共享代码——此时单个包就是正确选择,拆分还为时过早(参见反模式)。
- 这是 JS/pnpm 或 Cargo 工作区——此技能仅适用于 Python + uv。
工作流
第 1 步:决定是否拆分
只有在确实存在由两个或更多可部署项目使用的共享代码,或某个可部署项目能从可独立测试的核心中受益时,才拆分为多个成员。拆分信号:两个应用(例如 CLI 和 Web 服务)都需要相同的领域逻辑;某个应用只是可复用逻辑外面的一层薄壳。不应拆分的信号:
- 只有一个可部署项目,没有复用需求——保留为一个包。
- 成员需要相互冲突的依赖版本或独立的虚拟环境——uv 工作区共用一个环境和一个锁文件,因此此时应使用路径依赖,而不是工作区成员(参见第 2 步),或者拆分为独立仓库。
- 成员需要不同的
requires-python——uv 工作区会强制使用单一的requires-python(即所有成员要求的交集),因此不同的 Python 版本下限无法放在同一个工作区中。
拆分之后的典型结构:
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 add/uv run/锁文件的基础知识,请参阅 uv 技能;有关成员的内部模块布局,请参阅 python-project-structure。)
步骤 3:理顺依赖方向——依赖内层,保持无环
包依赖图必须是一个有向无环图(DAG),并且必须朝向共享核心向内:
- 应用/CLI 依赖共享库:
api → core、cli → core。 - 应用绝不能相互依赖:不能有
api → cli,也不能有cli → api。 - 共享库不依赖任何一个应用——
core没有应用依赖。共享代码如果“需要”应用中的某些内容,就说明该内容应该放在core(或另一个库)中,而不是让core向上导入。
跨应用或向上导入属于反模式:它会直接或间接造成循环依赖,并耦合本应独立发布的可部署组件。如果两个应用需要共享新内容,就应将其下沉到 core(或第二个库)中,而不要横向导入。
步骤 4:守住边界——uv 无法强制执行
uv 工作区共用一个环境,而Python 没有依赖隔离机制:在运行时,没有任何机制会阻止某个成员导入其兄弟成员声明的包(或模块)。uv 本身也指出,它“无法确保包不会导入由另一个工作区成员声明的依赖项”。因此,边界依靠约定 + 代码审查来维护,而不是依靠工具:
-
将横向/向上导入(步骤 3)视为阻止审查通过的问题。
-
在该成员自己的
pyproject.toml中声明它实际使用的每个依赖项——绝不要依赖兄弟成员的传递安装。 -
**可选的自动化强制检查——
import-linter。**它会在 CI / pre-commit 钩子中检查.importlinter契约,并在存在禁止导入时使构建失败。能表达这些规则的两种契约类型:- Independence——“这些模块不会相互导入”→ 应用彼此独立(没有
api ↔ cli)。 - Layers / Forbidden——分层顺序(应用在
core之上),或“禁止core导入应用”→ 强制依赖向内。
将它作为轻量级的安全网;无论是否使用它,上述规则都应遵守。
- Independence——“这些模块不会相互导入”→ 应用彼此独立(没有
第 5 步:在边界处定义每个成员的公共 API
使用方通过成员的公共接口(其包的 __init__ / __all__)导入成员,并将其他所有内容视为内部实现。因此:
- 在
libs/core/src/core/__init__.py中导出共享库预期提供的 API(使用__all__);使用方通过from core import X导入。 - 应用依赖的是这个边界,而不是
core的_internal模块——访问另一个成员的私有模块会让你再次依赖其内部实现,违背拆分的初衷。 - (包内的
__all__/ 下划线隐私机制属于python-project-structure技能的范畴;本技能仅补充跨成员规则:使用边界,而非内部实现。)
每个成员都使用 src 布局(libs/core/src/core/、apps/api/src/api/),让测试针对已安装的包运行,而不是误导入顶层目录中的包——所有成员保持一致。
第 6 步:放置测试和共享 fixture
- 每个成员都拥有自己的
tests/和conftest.py;pytest 会按目录自动发现 fixture。 - 对于确实需要跨成员共享的 fixture,优先选择以下方式之一:使用小型共享测试支持包(作为独立成员,或使用
tests/fixtures/模块),并通过pytest_plugins加载,而不是复制粘贴conftest。fixture 应尽可能靠近其所代表的对象;只有真正适用于整个仓库的 fixture 才放在顶层。 - 不要直接跨成员访问对方的
tests/目录;通过支持包共享。
第 7 步:安全地提取共享代码(拆分现有包时)
从现有应用中拆出库属于保持行为不变的重构——安全网优先:
- 先确保现有测试套件全部通过(它就是你的安全网)。
- 创建新的
libs/<core>成员(第 2 步),并配置应用依赖它。 - 移动共享模块到
core(只移动,不要重写);将应用中的导入改为from core import …。 - 运行完整的现有测试套件——测试数量应保持不变,且所有测试都必须继续通过。将已移动模块的单元测试移到
libs/core/tests/。 - 如果移动后出现循环依赖(移动的代码反向导入了应用,或应用需要某个库现在也需要的东西),则将共享依赖也移到
core,或反转依赖方向(将应用专属部分作为参数传入,或通过接口传入)来打破循环——绝不要通过向上导入来解决。
参见 references/worked-example.md,了解完整的三成员演练(根目录 + 库 + 两个应用)、.importlinter 合约,以及分步提取过程。
规则
硬性规则(绝不可违反):
- 跨包依赖图必须是向内指向的有向无环图(DAG):应用 → 共享库,绝不允许应用 ↔ 应用,且共享库不依赖任何应用。
- 每个成员都必须声明自己的依赖;绝不要依赖兄弟成员的传递安装。
- 使用方必须通过另一个成员的公共 API(
__init__/__all__)导入,绝不能导入其_internal模块。 - 成员之间通过
[tool.uv.sources] { <name> = { workspace = true } }建立依赖;如果成员无法共用一个环境或requires-python,那就不是一个工作区(请使用路径依赖或独立仓库)。 - 提取时必须安全网优先:现有测试套件通过 → 移动 → 改写导入 → 测试套件仍通过;出现循环依赖时,应通过下移代码来打破,绝不能向上导入。
偏好(可酌情调整):
- 每个成员都使用 src 布局,以保持基于安装包的测试发现方式一致。
- 当应用数量超过一个时,添加
import-linter合约(Independence + Layers/Forbidden),在 CI 中强制执行边界。 - 通过测试支持包 +
pytest_plugins共享 fixture,而不是复制conftest。 - 尽量精简成员数量;只有确实需要独立部署或确实需要复用时才添加成员。
常见陷阱
- uv 不会阻止错误的导入。 工作区共享一个 venv,因此在
api中执行from cli import …在运行时可以正常工作,尽管这违反了架构约束——uv 无法检测到。只有审查(或import-linter)才能发现。不要以为“能导入就说明允许”。 - 整个工作区只能有一个
requires-python。 uv 会取所有成员要求的交集;某个成员需要更高的 Python 版本下限时,会悄悄抬高所有成员的下限(或者导致它不适用于该工作区)。应在工作区层面确定 Python 版本下限。 - 访问同级成员的内部实现。
from core._internal.thing import x可以编译并运行,但会让你重新依赖可能随时变更的私有结构。只从边界导入。 - 过早拆分。 对没有实际复用需求的代码建立包含两个成员的工作区,只会平白增加锁文件、连线和边界维护成本。等复用需求或可独立测试的核心确实存在时再拆分,不要凭空预想。
- 提取后测试全绿,但结果仍然错误。 移动模块后,测试仍然全绿,可能会掩盖你为让测试通过而新增的反向导入。移动后要明确检查依赖方向,而不只是看测试结果。
- 忘记
[tool.uv.sources]。 在dependencies中列出core,却没有指定{ workspace = true }源,会让 uv 尝试从 PyPI 解析它——解析会失败,或者拉取别人发布的同名包。只有源配置行才能将它声明为工作区依赖。
反模式
- “我直接导入另一个应用的辅助函数就好。” 为了复用一个函数而横向进行
app → app导入。应将辅助函数下沉到共享库中;横向导入会造成循环依赖,并耦合可独立部署的应用。 - “库只要这一次从应用导入就行。” 任何向上的(lib → app)导入都会颠倒 DAG 的方向。库需要的内容应放在库中(或作为参数传入),绝不能向上获取。
- “反正都在同一个 venv 里,边界无关紧要。” 共享环境只是为了方便,不是跨越边界的许可——这正是为什么需要遵守规范并进行审查。
- “把每个模块都拆成独立的包吧。” 过度拆分:每个模块一个包会倍增 pyproject 和边界维护成本。一个成员应是可独立部署的应用或真正共享的库,而不是每个文件夹都算一个成员。
- “为了避免扩大公共 API,就直接访问
_internal。” 如果使用方需要某项内容,就应在边界处有意将其公开;导入私有内容只会造成隐性耦合。
输出
最终成果是一个决策与结构:成员拓扑(有哪些包以及原因)、根目录和各成员的 pyproject.toml 配置([tool.uv.workspace] + [tool.uv.sources])、明确的无环且依赖向内的方向、每个成员的公共 API 边界、测试与 fixture 布局,以及——在重构时——先建立安全网再进行提取的顺序。使用者是负责构建或重组工作区的工程师或编码 Agent;后续功能开发将以产出的结构为基础。
相关内容
uv— uv 的常规用法(uv add、uv run、锁文件)。本技能负责uv技能未涵盖的工作区配置;工作区之外的内容请使用uv。python-project-structure— 包内模块架构、__all__和分层结构。本技能涵盖其之上的跨包层;两者可搭配使用(本技能负责成员边界,另一技能负责成员内部结构)。import-linter(外部工具)— 可选的 CI 边界检查,通过.importlinter合约实现。
渐进式披露
references/worked-example.md— 完整的三成员工作区演练(根目录 + 共享库 + 两个应用及其pyproject.toml依赖关系)、.importlinter合约示例(独立性 + 分层),以及逐步安全提取流程。构建或重组具体工作区时加载。references/sources.md— 研究来源 / 引用。
正文预算
description≤ 1,024 个字符。Claude 会在列表中将description+when_to_use合计截断至 1,536 个字符。- 正文 ≤ 约 500 行 / 5,000 个 token。
- 单个参考文件:超过 10k 个 token 时发出警告,超过 25k 个 token 时报错。参考文件总计:超过 25k 个 token 时发出警告,超过 50k 个 token 时报错。