首页
/ uv 中的依赖声明方式:pyproject.toml 与 requirements.in 完整指南

uv 中的依赖声明方式:pyproject.toml 与 requirements.in 完整指南

2026-09-06 11:57:35作者:范垣楠Rhoda

声明依赖是 Python 工程化的起点。在 uv 中,最佳实践是把依赖声明放在静态文件中,而不是对虚拟环境做临时的 ad-hoc 安装;一旦依赖被定义,就可以通过锁定(lock)生成一致、可复现的环境。本文围绕 uv 官方文档中 Declaring dependencies 展开,讲清 pyproject.tomlrequirements.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 cliuv 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_dependenciesIndexMap<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 则会把 foo extra 下的依赖一并纳入锁定(对应测试见 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,该结构统一收集 requirementsconstraintsoverridesextras、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 -

注意两点:

  1. uv pip compile 默认只把结果打印到终端,需要 -o / --output-file 才写入文件;
  2. 锁定后,用 uv pip sync requirements.txt 可以让环境精确匹配锁文件;uv pip install 则不会移除环境中多出来的包,可复现性稍弱(详见 锁定环境文档)。

两种声明方式如何选择

  • pyproject.toml:适合标准 Python 项目。它既是构建配置([build-system])也是依赖声明([project]),支持 extras、requires-pythondynamic 等完整语义,是与 pip/PEP 621 生态兼容的推荐做法;
  • requirements.in:适合轻量场景——例如临时实验、数据科学脚本、只需锁定一份依赖清单而不需要打包发布的项目。它的优点是简单直接,代价是没有 extras 分组能力。

无论选择哪种方式,uv 提供的能力是一致的:静态声明 → 锁定精确版本 → 跨环境复现安装。掌握这一链路后,再配合 约束文件(constraints)覆盖文件(overrides) 等机制,就能覆盖绝大多数 Python 依赖管理的实际需求。

登录后查看全文
热门项目推荐
相关项目推荐