PEP 633 – 在 pyproject.toml 中使用展开的 TOML 表指定依赖项
- 作者:
- Laurie Opperman <laurie_opperman at hotmail.com>, Arun Babu Neelicattu <arun.neelicattu at gmail.com>
- 发起人:
- Brett Cannon <brett at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2020年9月2日
- 发布历史:
- 2020年9月2日
- 决议:
- Discourse 消息
拒绝通知
此 PEP 已被 PEP 631 取代,原因是 PEP 631 的受欢迎程度、与现有 PEP 508 字符串的用法一致性,以及与现有打包工具集的兼容性。
摘要
此 PEP 规定了如何使用 PEP 621 中定义的字段,在 pyproject.toml 文件中为打包相关工具编写项目依赖项,作为 PEP 631 中定义的基于 PEP 508 的方法的一种替代方案。
动机
使用 TOML 表和其他数据类型来表示依赖项而不是 PEP 508 字符串有许多优点:
- 通过 TOML 语法进行轻松的初始验证。
- 使用模式(例如 JSON Schema)进行轻松的二次验证。
- 用户有可能猜测给定功能的键,而不是死记硬背语法。
- 使用多种其他流行语言的用户可能已经熟悉 TOML 语法。
- TOML 直接表示与 JSON 中相同的数据结构,因此是 Python 字面量的子集,用户可以理解值的层次结构和类型。
基本原理
大部分内容取自 PEP 621 依赖项话题 中的讨论。其中包含了 Pipfile、Poetry、Dart 的依赖项 和 Rust 的 Cargo 的元素。一份 比较文档 显示了此格式与 PEP 508 风格的说明符之间的优缺点。
在指定具有相同发行版名称的多个依赖项(其中环境标记选择适当的依赖项)时,所选解决方案类似于 Poetry,允许使用依赖项的数组。
直接引用键与 PEP 610 和 PEP 440 密切对齐并利用它们,以减少打包生态系统中的差异并依赖于先前规范方面的工作。
规范
与 PEP 621 一样,如果元数据指定不当,工具必须引发错误。元数据必须符合 TOML 规范。
为了减少与本文件作为指定依赖项的规范相混淆,使用“requirement”(依赖项)一词来表示 PEP 508 依赖项规范。
将以下表添加到 PEP 621 中指定的 project 表中。
依赖项
格式:表
此表中的键是所需发行版的名称。值可以是以下类型之一:
- 字符串:依赖项仅由版本依赖项定义,其规范与依赖项表中的
version相同,但允许空字符串""表示不对版本进行任何限制。 - 表:一个依赖项表。
- 数组:依赖项表的数组。指定空数组
[]作为值是错误的。
依赖表
依赖项表的键如下(全部为可选):
version(字符串):一个 PEP 440 版本说明符,它是一个逗号分隔的版本说明符子句列表。字符串必须非空。extras(字符串数组):一个发行版的 PEP 508 额外依赖项声明列表。列表必须非空。markers(字符串):一个 PEP 508 环境标记表达式。字符串必须非空。url(字符串):用于安装和满足依赖项的工件的 URL。file://是用于从本地文件系统检索包的前缀。git、hg、bzr或svn(字符串):用于克隆的 VCS 存储库的 URL(如 PEP 440 中所述),其树将被安装以满足依赖项。其他 VCS 键将通过对 PEP 610 的修订来添加,但工具可以在接受修订之前选择使用其命令行命令来支持其他 VCS。revision(字符串):用于在安装之前签出以满足依赖项的指定 VCS 存储库的特定修订版本的标识符。用户仅当使用git、hg、bzr、svn或其他 VCS 键之一来标识要安装的发行版时,才必须提供此项。修订标识符在 PEP 610 中进行了建议。
最多只能同时指定以下键中的一个,因为它们在逻辑上与依赖项冲突:version、url、git、hg、bzr、svn 以及任何其他 VCS 键。
空依赖项表 {} 不会对依赖项施加任何限制,与空字符串 "" 效果相同。
提供的任何未在本文件中指定的键都必须导致解析错误。
可选依赖项
格式:表
此表中的键是一个额外依赖项所需的发行版的名称。值可以是以下类型之一:
- 表:一个依赖项表。
- 数组:依赖项表的数组。
这些依赖项表具有与上面相同的规范,并增加了以下必需键:
for-extra(字符串):此依赖项所需的 PEP 508 额外依赖项的名称。
参考实现
工具需要将此格式转换为 PEP 508 依赖项字符串。下面是一个该转换的示例实现(假设验证已完成):
def convert_requirement_to_pep508(name, requirement):
if isinstance(requirement, str):
requirement = {"version": requirement}
pep508 = name
if "extras" in requirement:
pep508 += " [" + ", ".join(requirement["extras"]) + "]"
if "version" in requirement:
pep508 += " " + requirement["version"]
if "url" in requirement:
pep508 += " @ " + requirement["url"]
for vcs in ("git", "hg", "bzr", "svn"):
if vcs in requirement:
pep508 += " @ " + vcs + "+" + requirement[vcs]
if "revision" in requirement:
pep508 += "@" + requirement["revision"]
extra = None
if "for-extra" in requirement:
extra = requirement["for-extra"]
if "markers" in requirement:
markers = requirement["markers"]
if extra:
markers = "extra = '" + extra + "' and (" + markers + ")"
pep508 += "; " + markers
return pep508, extra
def convert_requirements_to_pep508(dependencies):
pep508s = []
extras = set()
for name, req in dependencies.items():
if isinstance(req, list):
for sub_req in req:
pep508, extra = convert_requirement_to_pep508(name, sub_req)
pep508s.append(pep508)
if extra:
extras.add(extra)
else:
pep508, extra = convert_requirement_to_pep508(name, req)
pep508s.append(pep508)
if extra:
extras.add(extra)
return pep508s, extras
def convert_project_requirements_to_pep508(project):
reqs, _ = convert_requirements_to_pep508(project.get("dependencies", {}))
optional_reqs, extras = convert_requirements_to_pep508(
project.get("optional-dependencies", {})
)
reqs += optional_reqs
return reqs, extras
JSON Schema
对于初始验证,可以使用 JSON Schema。这不仅有助于工具进行一致的验证,还能让代码编辑器在用户构建依赖项列表时突出显示验证错误。
{
"$id": "spam",
"$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#",
"title": "Project metadata",
"type": "object",
"definitions": {
"requirementTable": {
"title": "Full project dependency specification",
"type": "object",
"properties": {
"extras": {
"title": "Dependency extras",
"type": "array",
"items": {
"title": "Dependency extra",
"type": "string"
}
},
"markers": {
"title": "Dependency environment markers",
"type": "string"
}
},
"propertyNames": {
"enum": [
"extras",
"markers",
"version",
"url",
"git",
"hg",
"bzr",
"svn",
"for-extra"
]
},
"oneOf": [
{
"title": "Version requirement",
"properties": {
"version": {
"title": "Version",
"type": "string"
}
}
},
{
"title": "URL requirement",
"properties": {
"url": {
"title": "URL",
"type": "string",
"format": "uri"
}
},
"required": [
"url"
]
},
{
"title": "VCS requirement",
"properties": {
"revision": {
"title": "VCS repository revision",
"type": "string"
}
},
"oneOf": [
{
"title": "Git repository",
"properties": {
"git": {
"title": "Git URL",
"type": "string",
"format": "uri"
}
},
"required": [
"git"
]
},
{
"title": "Mercurial repository",
"properties": {
"hg": {
"title": "Mercurial URL",
"type": "string",
"format": "uri"
}
},
"required": [
"hg"
]
},
{
"title": "Bazaar repository",
"properties": {
"bzr": {
"title": "Bazaar URL",
"type": "string",
"format": "uri"
}
},
"required": [
"bzr"
]
},
{
"title": "Subversion repository",
"properties": {
"svn": {
"title": "Subversion URL",
"type": "string",
"format": "uri"
}
},
"required": [
"svn"
]
}
]
}
]
},
"requirementVersion": {
"title": "Version project dependency specification",
"type": "string"
},
"requirement": {
"title": "Project dependency specification",
"oneOf": [
{
"$ref": "#/definitions/requirementVersion"
},
{
"$ref": "#/definitions/requirementTable"
},
{
"title": "Multiple specifications",
"type": "array",
"items": {
"$ref": "#/definitions/requirementTable"
},
"minLength": 1
}
]
},
"optionalRequirementTable": {
"title": "Project optional dependency specification table",
"allOf": [
{
"$ref": "#/definitions/requirementTable"
},
{
"properties": {
"for-extra": {
"title": "Dependency's extra",
"type": "string"
}
},
"required": [
"for-extra"
]
}
]
},
"optionalRequirement": {
"title": "Project optional dependency specification",
"oneOf": [
{
"$ref": "#/definitions/optionalRequirementTable"
},
{
"title": "Multiple specifications",
"type": "array",
"items": {
"$ref": "#/definitions/optionalRequirementTable"
},
"minLength": 1
}
]
}
},
"properties": {
"dependencies": {
"title": "Project dependencies",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/requirement"
}
},
"optional-dependencies": {
"title": "Project dependencies",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/optionalRequirement"
}
}
}
}
示例
完整的人工示例
[project.dependencies]
flask = { }
django = { }
requests = { version = ">= 2.8.1, == 2.8.*", extras = ["security", "tests"], markers = "python_version < '2.7'" }
pip = { url = "https://github.com/pypa/pip/archive/1.3.1.zip" }
sphinx = { git = "ssh://git@github.com/sphinx-doc/sphinx.git" }
numpy = "~=1.18"
pytest = [
{ version = "<6", markers = "python_version < '3.5'" },
{ version = ">=6", markers = "python_version >= '3.5'" },
]
[project.optional-dependencies]
pytest-timout = { for-extra = "dev" }
pytest-mock = [
{ version = "<6", markers = "python_version < '3.5'", for-extra = "dev" },
{ version = ">=6", markers = "python_version >= '3.5'", for-extra = "dev" },
]
为了向 PEP 631 致敬,以下是 docker-compose 的等效依赖项规范:
[project.dependencies]
cached-property = ">= 1.2.0, < 2"
distro = ">= 1.2.0, < 2"
docker = { extras = ["ssh"], version = ">= 4.2.2, < 5" }
docopt = ">= 0.6.1, < 1"
jsonschema = ">= 2.5.1, < 4"
PyYAML = ">= 3.10, < 6"
python-dotenv = ">= 0.13.0, < 1"
requests = ">= 2.20.0, < 3"
texttable = ">= 0.9.0, < 2"
websocket-client = ">= 0.32.0, < 1"
# Conditional
"backports.shutil_get_terminal_size" = { version = "== 1.0.0", markers = "python_version < '3.3'" }
"backports.ssl_match_hostname" = { version = ">= 3.5, < 4", markers = "python_version < '3.5'" }
colorama = { version = ">= 0.4, < 1", markers = "sys_platform == 'win32'" }
enum34 = { version = ">= 1.0.4, < 2", markers = "python_version < '3.4'" }
ipaddress = { version = ">= 1.0.16, < 2", markers = "python_version < '3.3'" }
subprocess32 = { version = ">= 3.5.4, < 4", markers = "python_version < '3.2'" }
[project.optional-dependencies]
PySocks = { version = ">= 1.5.6, != 1.5.7, < 2", for-extra = "socks" }
ddt = { version = ">= 1.2.2, < 2", for-extra = "tests" }
pytest = { version = "< 6", for-extra = "tests" }
mock = { version = ">= 1.0.1, < 4", markers = "python_version < '3.4'", for-extra = "tests" }
兼容性示例
此 PEP 的作者认识到各种工具都需要读取和写入此依赖项规范格式。本节旨在与当前使用的标准 PEP 508 进行直接比较,并提供转换的示例。
注意
为了简单和清晰,TOML 中指定每个规范的各种方法并未在此处展示。这些示例使用了标准的内联表示。
例如,虽然以下在 TOML 中被认为是等效的,但我们在本节的示例中选择了第二种形式。
aiohttp.version = "== 3.6.2"
aiohttp = { version = "== 3.6.2" }
版本约束的依赖项
无版本限制
aiohttp
aiohttp = {}
简单版本限制
aiohttp >= 3.6.2, < 4.0.0
aiohttp = { version = ">= 3.6.2, < 4.0.0" }
注意
为简洁起见,也可以将其表示为字符串。
aiohttp = ">= 3.6.2, < 4.0.0"
直接引用依赖项
URL 依赖项
aiohttp @ https://files.pythonhosted.org/packages/97/d1/1cc7a1f84097d7abdc6c09ee8d2260366f081f8e82da36ebb22a25cdda9f/aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl
aiohttp = { url = "https://files.pythonhosted.org/packages/97/d1/1cc7a1f84097d7abdc6c09ee8d2260366f081f8e82da36ebb22a25cdda9f/aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl" }
VCS 依赖项
aiohttp @ git+ssh://git@github.com/aio-libs/aiohttp.git@master
aiohttp = { git = "ssh://git@github.com/aio-libs/aiohttp.git", revision = "master" }
环境标记
aiohttp >= 3.6.1; python_version >= '3.8'
aiohttp = { version = ">= 3.6.1", markers = "python_version >= '3.8'" }
一个稍微扩展的上述示例,其中根据解释器版本要求特定版本的 aiohttp。
aiohttp >= 3.6.1; python_version >= '3.8'
aiohttp >= 3.0.0, < 3.6.1; python_version < '3.8'
aiohttp = [
{ version = ">= 3.6.1", markers = "python_version >= '3.8'" },
{ version = ">= 3.0.0, < 3.6.1", markers = "python_version < '3.8'" }
]
包的额外依赖项
为包的额外依赖项指定依赖项
aiohttp >= 3.6.2; extra == 'http'
aiohttp = { version = ">= 3.6.2", for-extra = "http" }
使用依赖项的额外依赖项
aiohttp [speedups] >= 3.6.2
aiohttp = { version = ">= 3.6.2", extras = ["speedups"] }
复杂示例
版本限制
aiohttp [speedups] >= 3.6.2; python_version >= '3.8' and extra == 'http'
aiohttp = { version = ">= 3.6.2", extras = ["speedups"], markers = "python_version >= '3.8'", for-extra = "http" }
直接引用(VCS)
aiohttp [speedups] @ git+ssh://git@github.com/aio-libs/aiohttp.git@master ; python_version >= '3.8' and extra == 'http'
aiohttp = { git = "ssh://git@github.com/aio-libs/aiohttp.git", revision = "master", extras = ["speedups"], markers = "python_version >= '3.8'", for-extra = "http" }
被拒绝的想法
将 dependencies 更改为数组
使用数组而不是表,以便每个元素仅为一个表(带有一个 name 键),而不是依赖项表数组。这在 TOML 格式中非常冗长且受限,而且一个发行版有多个依赖项的情况并不常见。
将 optional-dependencies 替换为 extras
移除 optional-dependencies 表,改为在依赖项中包含一个 optional 键,以及一个 extras 表,该表指定了项目额外依赖项所需的(可选)依赖项。这减少了具有相同规范的表数量(至 1 个),并允许依赖项被指定一次但用于多个额外依赖项,但将某些依赖项的属性(它属于哪个额外依赖项)分开,将必需和可选依赖项分组在一起(可能混合),并且可能没有简单的方法来选择一个依赖项,当一个发行版有多个依赖项时。此提议被拒绝,因为 optional-dependencies 已在 PEP 621 草案中使用。
依赖项中的 direct 表
将直接引用键包含在 direct 表中,将 VCS 指定为 vcs 键的值。这更明确,更容易包含在 JSON Schema 验证中,但被认为过于冗长且可读性不高。
包含哈希
在直接引用依赖项中包含哈希。这仅用于包锁定文件,在项目的元数据中没有实际位置。
每个额外依赖项的依赖项表
将 optional-dependencies 设为一个依赖项表表,每个表对应一个额外依赖项,表名为该额外依赖项的名称。这使得 optional-dependencies 与 dependencies(依赖项表)具有不同的类型(表套表),这可能会让用户感到困惑且更难解析。
环境标记键
将每个 PEP 508 环境标记作为依赖项中的键(或子表键)。这可以提高可读性和解析的简便性。仍然允许使用 markers 键来进行更高级的规范,其中键指定的环境标记会与结果进行“与”运算。这被推迟了,因为需要更多的设计。
一个依赖项可以满足的多个额外依赖项
将 for-extra 键替换为 for-extras,其值为依赖项所满足的额外依赖项的数组。这减少了一些重复,但在这种情况下,这种重复使得哪个额外依赖项具有哪些依赖项变得更加明确。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0633.rst