编程学习网 > 编程语言 > Python > UV Workspace:一条命令管好所有 Python 子包!
2026
07-24

UV Workspace:一条命令管好所有 Python 子包!


项目里一共四个 Python 子包,仓库根目录却躺着六份 requirements.txt

更离谱的是,公共库改了一行代码,订单服务里没生效。查了半天,发现有人装的是本地 editable 包,有人装的是私服上个月的版本,还有一个人的虚拟环境压根没切对。

这种仓库我一般不急着看业务代码,先看依赖怎么管。依赖关系已经乱了,代码跑出什么结果都不奇怪。

以前处理 Python 多包项目,经常是这个样子:

trade-platform/
├── order_api/
│   ├── requirements.txt
│   └── .venv/
├── settlement_job/
│   ├── requirements.txt
│   └── .venv/
├── packages/
│   └── biz_rules/
│       ├── requirements.txt
│       └── .venv/
└── scripts/
    └── install_local.sh

order_api 要调用 biz_rules,开发机先执行:

pip install -e ./packages/biz_rules

CI 里再写一遍,Dockerfile 里还得复制一遍。目录稍微调整一下,脚本全红。

UV Workspace 处理的就是这类问题:一个 Git 仓库放多个 Python 项目,每个子包保留自己的 pyproject.toml,但整个工作区共用一份 uv.lockuv lock 会解析整个工作区,uv runuv sync 也能通过 --package 指定某个子包。

先把目录收一下:

trade-platform/
├── pyproject.toml
├── uv.lock
├── services/
│   ├── order-api/
│   │   ├── pyproject.toml
│   │   └── src/order_api/
│   └── settlement-job/
│       ├── pyproject.toml
│       └── src/settlement_job/
└── packages/
    └── biz-rules/
        ├── pyproject.toml
        └── src/biz_rules/

根目录的配置不用写得很花:

[project]
name = "trade-platform"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[tool.uv.workspace]
members = [
    "services/*",
    "packages/*",
]

members 支持 glob。被匹配到的目录必须有自己的 pyproject.toml,不想纳入管理的临时项目,可以再配 exclude

公共规则包里放真正需要复用的东西。别把数据库连接、全局配置这些也塞进去,最后公共包长成第二个单体应用。

# packages/biz-rules/src/biz_rules/pricing.py
from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True, slots=True)
class PriceLine:
    sku: str
    amount: Decimal

def calculate_payable(lines: list[PriceLine]) -> Decimal:
    total = sum(
        (line.amount for line in lines),
        start=Decimal("0"),
    )
    if total <= 0:
        raise ValueError("订单应付金额必须大于 0")
    return total.quantize(Decimal("0.01"))

订单服务依赖这个包,不能只在代码里直接 import。这种写法我见过不少,本机能跑,换台机器立刻报错,因为依赖根本没声明。

 services/order-api/pyproject.toml 里写清楚:

[project]
name = "order-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "biz-rules",
    "fastapi>=0.115",
]
[tool.uv.sources]
biz-rules = { workspace = true }

workspace = true 表示这个依赖从当前 Workspace 里找,不去 PyPI 碰运气。工作区成员之间默认按 editable 方式安装,公共包代码改完,不需要每次重新打 wheel

不想手改配置,也可以直接在根目录执行:

uv add --package order-api ./packages/biz-rules
uv add --package order-api fastapi

这里的 --package order-api 很重要。它改的是订单服务自己的依赖,不是把所有东西都堆到根项目里。

配置完成后,整个仓库第一次拉下来只需要:

uv sync

虚拟环境、第三方依赖、本地子包和 uv.lock 一起处理。UV 在执行 uv run 时也会自动检查锁文件和环境是否需要同步,这比先激活环境,再装依赖,再祈祷版本一致省事得多。

启动某个服务也不用 cd  cd 去:

uv run --package order-api python -m order_api.main

跑结算任务:

uv run --package settlement-job python -m settlement_job.runner

查订单服务到底拉进来了哪些依赖:

uv tree --package order-api

我比较喜欢把 CI 也收成同一套命令:

uv sync --locked
uv run --package order-api pytest services/order-api/tests
uv run --package settlement-job pytest services/settlement-job/tests

--locked 的意思不是更新锁文件,而是要求当前配置必须和锁文件一致。有人改了 pyproject.toml 却没提交新的 uv.lockCI 直接失败,别让它悄悄解析出另一套版本。

不过 Workspace 也别乱套。

它共享一套依赖解析和工作区环境,不是给每个子包造一堵完全隔离的墙。订单服务要求 pydantic<2,结算任务非要 pydantic>=2,这种明显冲突还硬塞进一个 Workspace,最后只会和解析器较劲。UV 官方也明确提醒,成员依赖冲突严重,或者必须使用独立虚拟环境时,普通路径依赖往往更合适。

所以我判断一个仓库要不要上 Workspace,看的不是子目录多不多,而是这些子包是不是同一套工程的一部分。

它们一起开发、一起测试、需要引用本地公共库,版本也能协商,那就放进去。最后仓库里留一份锁文件、一套命令,谁再提交第五份 requirements.txt,代码评审里直接打回去。

以上就是“UV Workspace:一条命令管好所有 Python 子包!的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。 

扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取