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 討論串
- 狀態:
- 已否決 (Rejected)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2020年9月2日
- 公告歷史:
- 2020年9月2日
- 決議:
- Discourse 訊息
拒絕通知
此 PEP 已被否決,轉而支持 PEP 631,原因在於其普及性、與現有 PEP 508 字串用法的一致性,以及與現有打包工具套件的相容性。
摘要
此 PEP 規定如何在 pyproject.toml 檔案中撰寫專案的依賴項,供打包相關工具使用,其中欄位定義在 PEP 621 中。這是一種替代方案,用以取代在 PEP 631 中定義的基於 PEP 508 的方法。
動機
使用 TOML 表和其他資料型別來表示依賴項,而非 PEP 508 字串,具有多項優點。
- 透過 TOML 語法,易於進行初步驗證。
- 使用結構描述,例如 JSON 結構描述,易於進行次要驗證。
- 使用者可能可以猜測給定功能的鍵,而非記憶語法。
- 其他多種流行語言的使用者可能已熟悉 TOML 語法。
- TOML 直接表示與 JSON 中相同的資料結構,因此是 Python 字面值的子集,這樣使用者就能理解層次結構和值的型別
原理
大部分內容取自 PEP 621 依賴項主題中的討論。其中包含來自 Pipfile、Poetry、Dart 的依賴項和 Rust 的 Cargo 的元素。一份 比較文件顯示了此格式與 PEP 508 風格指定器之間的優缺點。
在規範具有相同發行名稱的多個依賴項時 (其中環境標記選擇適當的依賴項),所選擇的解決方案與 Poetry 的類似,即允許使用依賴項陣列。
直接參考鍵與 PEP 610 和 PEP 440 緊密對齊並利用其內容,以減少打包生態系統中的差異,並依賴先前的規範工作。
規範
如 PEP 621 中所述,如果中繼資料指定不當,則工具必須 (MUST) 引發錯誤。中繼資料必須 (MUST) 符合 TOML 規範。
為減少本文件作為指定依賴項的規範所引起的混淆,「依賴項」(requirement) 一詞在此表示 PEP 508 的依賴項規範。
以下表格被新增至 PEP 621 中指定的 project 表。
dependencies
格式:表
此表中的鍵是所需發行套件的名稱。值可以具有以下型別之一
- 字串:依賴項僅由版本需求定義,規範與依賴項表中的
version相同,但允許空字串""對版本不施加任何限制。 - 表:一個依賴項表。
- 陣列:一個依賴項表陣列。將空陣列
[]指定為值是錯誤的。
依賴項表
依賴項表的鍵如下 (所有皆為選用)
version(字串):一個 PEP 440 版本指定器,它是一個以逗號分隔的版本指定子句列表。此字串必須 (MUST) 非空。extras(字串陣列):此發行套件的 PEP 508 額外功能宣告列表。此列表必須 (MUST) 非空。markers(字串):一個 PEP 508 環境標記表達式。此字串必須 (MUST) 非空。url(字串):要安裝並滿足依賴項的構件 (artifact) 的 URL。請注意,file://是用於從本地檔案系統檢索套件的前綴。git、hg、bzr或svn(字串):一個 VCS 儲存庫的 URL (如 PEP 440 中指定) 用於複製,其樹狀結構將被安裝以滿足依賴項。未來的 VCS 鍵將透過 PEP 610 的修訂來添加,然而,在修訂被接受之前,工具可以 (MAY) 選擇使用其命令列指令來支援其他 VCS。revision(字串):在安裝前要檢出的指定 VCS 儲存庫的特定修訂版本識別碼。使用者必須 (MUST) 僅在git、hg、bzr、svn或其他 VCS 鍵用於識別要安裝的發行套件時提供此項。PEP 610 中建議了修訂版本識別碼。
以下鍵中最多只能同時指定一個,因為它們在依賴項中邏輯上相互衝突:version、url、git、hg、bzr、svn,以及任何其他 VCS 鍵。
空的依賴項表 {} 對依賴項不施加任何限制,與空字串 "" 的作用相同。
任何未在本文件中指定的鍵,必須 (MUST) 導致解析錯誤。
選用依賴項
格式:表
此表中的鍵是某個額外功能所需的發行套件名稱。值可以具有以下型別之一
- 表:一個依賴項表。
- 陣列:一個依賴項表陣列。
這些依賴項表具有 與上方相同的規範,並新增以下必填鍵
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 結構描述
為了進行初步驗證,可以使用 JSON 結構描述。這不僅有助於工具保持一致的驗證,還能讓程式碼編輯器在使用者建構依賴項列表時,突顯驗證錯誤。
{
"$id": "spam",
"$schema": "http://json-schema.org/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 格式中非常冗長且限制性強,而且對於給定發行套件具有多個依賴項並不常見。
以 extras 取代 optional-dependencies
移除 optional-dependencies 表,轉而支持在依賴項中包含 optional 鍵,以及一個 extras 表,用於指定專案額外功能所需的 (選用) 依賴項。這減少了具有相同規範的表數量 (至 1 個),並允許依賴項只指定一次,但在多個額外功能中使用;但它使依賴項的某些屬性 (它屬於哪個額外功能) 變得不那麼直接,將必填和選用依賴項歸類在一起 (可能混淆),而且當一個發行套件有多個依賴項時,可能沒有簡單的方法來選擇一個依賴項。此方案被否決,因為 optional-dependencies 已在 PEP 621 草案中使用。
依賴項中的 direct 表
將直接參考鍵包含在 direct 表中,並將 VCS 指定為 vcs 鍵的值。這更加明確,也更容易納入 JSON 結構描述驗證中,但最終被認為過於冗長且可讀性不佳。
包含雜湊值
在直接參考依賴項中包含雜湊值。這僅用於套件鎖定檔案,並且在專案的中繼資料中並沒有真正的位置。
針對每個額外功能的依賴項表
讓 optional-dependencies 成為由每個額外功能的依賴項表組成的表,其中表名稱為額外功能的名稱。這使得 optional-dependencies 的型別 (由依賴項表組成的表) 不同於 dependencies (依賴項表),這可能會讓使用者感到不適應,並且更難以解析。
環境標記鍵
將每個 PEP 508 環境標記作為依賴項中的鍵 (或子表鍵)。這可說增加了可讀性和解析的便利性。markers 鍵仍將允許用於更進階的規範,其中,透過鍵指定的環境標記會與其餘結果進行邏輯 AND 運算。此方案被延後,因為需要進行更多設計。
一個依賴項即可滿足的多個額外功能
以 for-extras 取代 for-extra 鍵,其中值是一個依賴項所滿足的額外功能陣列。這減少了一些重複,但在這種情況下,該重複明確了哪些額外功能具有哪些依賴項。
版權
本文件已進入公有領域或遵循 CC0-1.0-Universal 授權,以較寬鬆者為準。
來源:https://github.com/python/peps/blob/main/peps/pep-0633.rst
上次修改時間:2025-02-01 08:55:40 GMT