跳转至

UV Workspace 完全指南:轻松管理多子项目Python依赖

最近在看 google A2A 协议,它在官方的github 仓库有一些示例, https://github.com/google/A2A/tree/main/samples/python ,这些示例使用了uv ,并且用了 workspace,这个是之前使用uv 时没有接触过的,本文记录以下在uv 中如何使用 workspace 。

workspace 的定义

UV 的 Workspace 功能是用于管理包含多个子项目的 Python 代码库的工具,其设计灵感来自 Rust 的 Cargo。

1. 核心概念

  • 共享依赖管理:工作区内的所有子项目共享同一个虚拟环境和锁文件(uv.lock),确保依赖一致性。
  • 独立配置:每个子项目可定义自己的 pyproject.toml,但通过工作区根目录的配置统一协调。

2. 有什么好处

关于使用 workspace 有什么好处呢? 我认为最主要的是可以共享项目依赖,拿上面提到的 A2A sample,以下是目录结构(agents 目录下做了以写删减)

1
2
3
4
5
6
7
8
9
.
├── README.md
├── agents
   ├── ag2
   ├── crewai
├── common
├── hosts
├── pyproject.toml
└── uv.lock

这个项目的根目录下有common 和hosts 两个目录,这里的函数或者类在 agents 下各种不同的agent 框架都需要用的到的,而 agents 下的 crewai 和ag2 是两个不同的框架,虽然他们都需要目录下的 common 和 hosts ,但是他们的底层是不同的,如果用户只想运行 ag2 这个例子,那他完全没有必要同步 crewai 下的依赖

[!warning] crewai 的依赖非常多!!!

如果没有用到 workspace, 那么需要将每个agent 目录抽出来形成单独的仓库,并且根目录下的common 和 hosts 需要复制到每个仓库中,如果哪天common 下的函数修改了,那么还需要同步到所有子项目中。

说了那么多,让我们来看一下如何在uv 中使用workspace 功能? A2A 官方那个实例有点复杂,我使用命令行从头开始创建一个新的uv 项目。

实战一下

1
2
3
4
uv init root_project  # 创建根项目
cd my_project
uv init service_a  # 创建子项目
uv init service_b

此时的root_project 的pyproject.toml 内容

[project]
name = "root-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []

[tool.uv.workspace]
members = [
    "service_a",
    "service_b",
]

整个目录文件结构为

.
├── README.md
├── main.py
├── pyproject.toml
├── service_a
   ├── README.md
   ├── main.py
   └── pyproject.toml
├── service_b
   ├── README.md
   ├── main.py
   └── pyproject.toml
└── uv.lock

在一个uv 的项目中使用 uv init 初始化项目,这个项目会自动添加到根项目的 tool.uv.workspace 的members 中。 这个配置很重要!

[!notice] 尽量使用uv 自动管理 最好不要手工修改pyproject.toml 文件,除非迫不得已

在root_project 下的pyproject.toml 中添加两个依赖 (也可以先使用 uv sync 创建好虚拟环境,然后再使用 uv add 添加依赖)

1
2
3
4
dependencies = [
    "httpx>=0.28.1",
    "httpx-sse>=0.4.0",
]

使用 uv sync 同步依赖,此时会将刚才配置的 httpx 和 httpx-sse 依赖安装好。

在子项目中就可以使用 httpx 库了,而不需要在子项目的pyproject.toml 中再次添加 httpx 依赖

子项目单独的依赖

在根项目中定义的依赖,所有工作区内的项目都可以直接使用,那么如果工作区需要用到单独的依赖呢?

先 cd 到子项目中

cd service_a
uv add requests

此时service_a 中的 pyproject.toml 中已经自动添加了依赖

1
2
3
4
5
6
7
8
9
[project]
name = "service-a"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.32.3",
]

但是此时,并没有在 service_a 中创建 .venv 虚拟环境,新添加的requests 库其实安装到了根项目的 .venv 中,相应的文件版本信息也写到了 uv.lock 文件中。

同样的操作在 service_b 中添加 pydantic 包。

回到 root_project 目录下运行 uv tree 查看依赖关系

(root-project)   root_project git:(master)  uv tree
Resolved 19 packages in 29ms
service-b v0.1.0
└── pydantic v2.11.4
    ├── annotated-types v0.7.0
    ├── pydantic-core v2.33.2
       └── typing-extensions v4.13.2
    ├── typing-extensions v4.13.2
    └── typing-inspection v0.4.0
        └── typing-extensions v4.13.2
service-a v0.1.0
└── requests v2.32.3
    ├── certifi v2025.4.26
    ├── charset-normalizer v3.4.2
    ├── idna v3.10
    └── urllib3 v2.4.0
root-project v0.1.0
├── httpx v0.28.1
   ├── anyio v4.9.0
      ├── idna v3.10
      ├── sniffio v1.3.1
      └── typing-extensions v4.13.2
   ├── certifi v2025.4.26
   ├── httpcore v1.0.9
      ├── certifi v2025.4.26
      └── h11 v0.16.0
   └── idna v3.10
└── httpx-sse v0.4.0

其实此时service_a 和 service_b 还有roo_project 三个项目公用的是同一套虚拟环境,service_a 和 service_b 可以引用根项目的模块。

[!success] 最佳实践 既然使用了 workspace,那么就不要让子项目互相引用,如 service_a 引用service_b 中的类,如果引用了,那么就失去了子项目的意义了!

如何同步依赖

先将之前创建的虚拟环境删除掉,模拟从github 仓库下载的项目

现在三个pyproject.toml 分别定义如下

root_project:

[project]
name = "root-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "httpx>=0.28.1",
    "httpx-sse>=0.4.0",
]

[tool.uv.workspace]
members = [
    "service_a",
    "service_b",
]

service_a:

1
2
3
4
5
6
7
8
9
[project]
name = "service-a"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.32.3",
]

service_b:

1
2
3
4
5
6
7
8
9
[project]
name = "service-b"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "pydantic>=2.11.4",
]

此时我在项目根目录下运行 uv sync 命令

root_project git:(master)  uv sync
Using CPython 3.11.10 interpreter at: /opt/miniconda3/bin/python3.11
Creating virtual environment at: .venv
Resolved 19 packages in 51ms
Installed 9 packages in 105ms
 + anyio==4.9.0
 + certifi==2025.4.26
 + h11==0.16.0
 + httpcore==1.0.9
 + httpx==0.28.1
 + httpx-sse==0.4.0
 + idna==3.10
 + sniffio==1.3.1
 + typing-extensions==4.13.2

可以看到只安装了root_project 中定义的 httpx 和 httpx-sse相关的依赖,servive_a 中定义的 requests 和 service_b 中定义的 pydantic 均没有下载。

查看官方的解释

By default, uv run and uv sync operates on the workspace root. For example, in the above example, uv run and uv run --package albatross would be equivalent, while uv run --package bird-feeder would run the command in the bird-feeder package.

也就是说:uv run 和 uv sync 默认在工作区根目录下运行,那么如果我想即安装根目录下的依赖,又安装 service_a 项目的依赖,应该怎么操作呢?

上面的文档中也说明了,可以通过 --package 参数指定包名,包名又是如何获取的?

查看 service_a 的 pyproject.toml 定义

1
2
3
4
[project]
name = "service-a"
version = "0.1.0"
...

这里的 name 就是包名,可以使用 uv sync --package service-a 来同步依赖

root_project git:(master)  uv sync --package service_a
Resolved 19 packages in 3ms
Uninstalled 7 packages in 68ms
Installed 3 packages in 111ms
 - anyio==4.9.0
 + charset-normalizer==3.4.2
 - h11==0.16.0
 - httpcore==1.0.9
 - httpx==0.28.1
 - httpx-sse==0.4.0
 + requests==2.32.3
 - sniffio==1.3.1
 - typing-extensions==4.13.2
 + urllib3==2.4.0

可以看到,这次是把service_a 中的requests 安装上了,但是也把roo_project 中的 httpx 和 httpx-sse 也给删掉了 😓

使用 uv sync 是不行了,目前没有很好的方法可以解决这个问题,或者我还不知道,如果有清楚怎么操作的请留言指出。

不过uv sync 还有一个 --all-packages 参数,可以同步所有的依赖包,使用这个命令就可以同时安装root_project 和 它下面的所有子项目依赖

root_project git:(master)  uv sync --all-packages
Resolved 19 packages in 26ms
Installed 11 packages in 102ms
 + annotated-types==0.7.0
 + anyio==4.9.0
 + h11==0.16.0
 + httpcore==1.0.9
 + httpx==0.28.1
 + httpx-sse==0.4.0
 + pydantic==2.11.4
 + pydantic-core==2.33.2
 + sniffio==1.3.1
 + typing-extensions==4.13.2
 + typing-inspection==0.4.0

但是这种方式,会安装好的额外的包,也不是很方便。

这里提供一个解决方案

进到相应的子项目目录,然后使用 uv run xxxx.py ,先运行一下这个项目,这时uv 就会先同步这个子项目的依赖,然后再运行相应的脚本。

这种方法,不会像 uv sync 那样删除掉root_project 下的依赖。

root_project git:(master)  uv sync # 在根目录下运行 uv sync,同步根项目依赖
Using CPython 3.11.10 interpreter at: /opt/miniconda3/bin/python3.11
Creating virtual environment at: .venv
Resolved 19 packages in 24ms
Installed 9 packages in 75ms
 + anyio==4.9.0
 + certifi==2025.4.26
 + h11==0.16.0
 + httpcore==1.0.9
 + httpx==0.28.1
 + httpx-sse==0.4.0
 + idna==3.10
 + sniffio==1.3.1
 + typing-extensions==4.13.2
root_project git:(master)  source .venv/bin/activate # 激活虚拟环境
(root-project)   root_project git:(master)  cd service_b # cd 到子项目
(root-project)   service_b git:(master)  uv run main.py 
Installed 4 packages in 54ms  # 自动安装依赖
Hello from service-b!

总结

本文介绍了在uv 中如何使用workspace 来管理主项目和子项目,以及在下载一个含有 workspace 的项目以后如何同步依赖,上面的示例很简单,如果感兴趣的可以查看A2A 仓储的sample,这个也不是太复杂,相信看完此文章,再查看sample 就会清晰很多了。