uv 中的依赖声明方式:pyproject.toml 与 requirements.in 完整指南
【免费下载链接】uvAn extremely fast Python package and project manager, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/uv/uv
声明依赖是 Python 工程化的起点。在 uv 中,最佳实践是把依赖声明放在静态文件中,而不是对虚拟环境做临时的 ad-hoc 安装;一旦依赖被定义,就可以通过锁定(lock)生成一致、可复现的环境。本文围绕 uv 官方文档中 Declaring dependencies 展开,讲清pyproject.toml与requirements.in两种声明方式的写法、可选依赖(extras)的语义差异,并结合 uv 仓库源码说明这些声明是如何被解析的。
为什么用静态文件声明依赖
uv 的 dependencies 文档开篇就给出核心原则:依赖应当声明在静态文件中,原因是:
- 可复现:静态声明文件可以随代码库版本化,团队成员和 CI 看到的依赖定义完全一致;
- 可锁定:声明之后可以用
uv pip compile将依赖锁定到精确版本,生成可复现的安装清单。文档中的链接指向 锁定环境指南:
$ uv pip compile pyproject.toml -o requirements.txt- 可审计:文件化的依赖便于审查与 diff,避免"环境里到底装了什么"只存在于某个开发者的本地机器上。
声明完依赖后,后续的完整链路是:声明(pyproject.toml/requirements.in)→ 锁定(uv pip compile生成requirements.txt)→ 安装/同步(uv pip install/uv pip sync)。
使用 pyproject.toml 声明依赖
pyproject.toml是 Python 标准中用于定义项目配置的文件。uv 遵循 PEP 621 来读取其中的project表。
声明基础依赖
在pyproject.toml中定义项目依赖:
[project] dependencies = [ "httpx", "ruff>=0.3.0" ]每个条目都是标准的 PEP 508 需求说明符(requirement specifier),因此可以带版本约束、环境标记(markers)等,例如ruff>=0.3.0表示安装不低于 0.3.0 的版本。
声明可选依赖(extras)
可选依赖定义在[project.optional-dependencies]表中:
[project.optional-dependencies] cli = [ "rich", "click", ]这里的每个键(key)定义一个extra(本例中为cli)。使用 uv 时,有三种安装/锁定这些可选依赖的方式:
- 用
--extra标志按需启用:uv pip install --extra cli或uv pip compile pyproject.toml --extra cli; - 用
--all-extras一次性启用所有 extras; - 用 PEP 508 的
package[<extra>]语法在依赖中直接引用,例如"my-package[cli]"。
更详细的安装用法见 从文件安装包。
源码视角:uv 如何解析 pyproject.toml
从 uv 的实现看,pyproject.toml的解析入口在 uv-pypi-types 的元数据模块:
/// PEP 621 project metadata. #[derive(Deserialize, Debug, Clone)] #[serde(try_from = "PyprojectTomlWire")] pub struct Project { /// The name of the project pub name: PackageName, /// The version of the project as supported by PEP 440 pub version: Option<Version>, /// The Python version requirements of the project pub requires_python: Option<String>, /// Project dependencies pub dependencies: Option<Vec<String>>, /// Optional dependencies pub optional_dependencies: Option<IndexMap<ExtraName, Vec<String>>>, /// Specifies which fields listed by PEP 621 were intentionally unspecified pub dynamic: Option<Vec<String>>, }(来源:crates/uv-pypi-types/src/metadata/pyproject_toml.rs)
几个值得注意的实现细节:
optional_dependencies是IndexMap<ExtraName, Vec<String>>:即"extra 名 → 依赖字符串列表"的有序映射。文档示例中cli = ["rich", "click"]会被解析为键cli映射到两个依赖项。ExtraName来自uv-normalize,意味着 extra 名会经过规范化处理(大小写不敏感等);dynamic字段的特殊含义:如果dependencies出现在dynamic列表中,说明依赖不是静态可知的,需要运行构建后端才能获取。specification.rs 的模块文档解释了 uv 对此的分流逻辑——静态pyproject.toml会直接读取dependencies并丢弃目录本身;动态的则把目录加入source_trees,通过 PEP 517 调用构建后端获取元数据;requires-python也参与解析:PyProjectToml::requires_python()会在requires-python被列入dynamic时拒绝提供(返回DynamicField错误),说明 uv 要求用于版本过滤的requires-python必须是静态可得的。
使用 requirements.in 声明依赖
除pyproject.toml外,Python 生态中常见的另一种做法是用轻量的requirements 文件格式(每行一个依赖)来声明项目依赖。uv 文档建议将文件命名为requirements.in,以便与锁定后生成的requirements.txt区分开。
基本写法
httpx ruff>=0.3.0每一行是一个独立的需求条目。
关键限制:不支持可选依赖分组
requirements.in格式不支持可选依赖分组(extras groups)。这是它与pyproject.toml的重要语义差异:
| 特性 | pyproject.toml | requirements.in |
|---|---|---|
| 标准项目配置 | 是(PEP 621) | 否 |
| 可选依赖 / extras | 支持([project.optional-dependencies]) | 不支持 |
--extra/--all-extras锁定 | 支持 | 不适用 |
| 定位 | 项目元数据 + 依赖声明 | 轻量依赖清单 |
因此在uv pip compile requirements.in时无法使用--extra来启用额外依赖组;而uv pip compile pyproject.toml --extra foo则会把fooextra 下的依赖一并纳入锁定(对应测试见 pip_compile 集成测试中 --extra 相关用例)。
源码视角:requirements 行如何被解析
requirements 文件的解析实现在 uv-requirements-txt crate 中。其中每一行的需求通过 requirement.rs 中的RequirementsTxtRequirement表示:
/// A requirement specifier in a `requirements.txt` file. pub enum RequirementsTxtRequirement { /// The uv-specific superset over PEP 508 requirements specifier /// incorporating `tool.uv.sources`. Named(uv_pep508::Requirement<VerbatimParsedUrl>), /// A PEP 508-like, direct URL dependency specifier. Unnamed(UnnamedRequirement<VerbatimParsedUrl>), }解析流程是:先尝试按 PEP 508 语法解析该行(即ruff>=0.3.0这类"名称 + 版本约束"形式),解析成功则记为Named;否则按"无名"的直接 URL 依赖(例如裸 URL 行)解析为Unnamed。这与文档示例中httpx(纯名称)和ruff>=0.3.0(名称 + 版本约束)两类条目对应。
所有解析结果最终汇入 uv-requirements 的规格结构RequirementsSpecification,该结构统一收集requirements、constraints、overrides、extras、index 配置等字段,是uv pip compile/uv pip install -r等命令共享的输入抽象。从该结构的字段定义可以推断:extras 是以FxHashSet<ExtraName>集合形式整体收集的,这与"--extra是一个"启用/不启用"的集合语义"相一致。
从声明到锁定:完整的操作链路
结合 锁定环境文档,声明文件与锁定命令的对应关系如下:
# 锁定 pyproject.toml 中声明的依赖 $ uv pip compile pyproject.toml -o requirements.txt # 锁定 requirements.in 中声明的依赖 $ uv pip compile requirements.in -o requirements.txt # 锁定多个文件 $ uv pip compile pyproject.toml requirements-dev.in -o requirements-dev.txt # 启用指定 extra(仅 pyproject.toml 支持) $ uv pip compile pyproject.toml --extra cli $ uv pip compile pyproject.toml --all-extras # 标准输入 $ echo "ruff" | uv pip compile -注意两点:
uv pip compile默认只把结果打印到终端,需要-o/--output-file才写入文件;- 锁定后,用
uv pip sync requirements.txt可以让环境精确匹配锁文件;uv pip install则不会移除环境中多出来的包,可复现性稍弱(详见 锁定环境文档)。
两种声明方式如何选择
pyproject.toml:适合标准 Python 项目。它既是构建配置([build-system])也是依赖声明([project]),支持 extras、requires-python、dynamic等完整语义,是与 pip/PEP 621 生态兼容的推荐做法;requirements.in:适合轻量场景——例如临时实验、数据科学脚本、只需锁定一份依赖清单而不需要打包发布的项目。它的优点是简单直接,代价是没有 extras 分组能力。
无论选择哪种方式,uv 提供的能力是一致的:静态声明 → 锁定精确版本 → 跨环境复现安装。掌握这一链路后,再配合 约束文件(constraints)与 覆盖文件(overrides) 等机制,就能覆盖绝大多数 Python 依赖管理的实际需求。
【免费下载链接】uvAn extremely fast Python package and project manager, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/uv/uv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考