PEP 518 – 為 Python 專案指定最小構建系統需求
- 作者:
- Brett Cannon <brett at python.org>, Nathaniel J. Smith <njs at pobox.com>, Donald Stufft <donald at stufft.io>
- BDFL-Delegate:
- Alyssa Coghlan
- 討論於:
- Distutils-SIG 郵件列表
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2016年5月10日
- 公告歷史:
- 2016年5月10日, 2016年5月11日, 2016年5月13日
- 決議:
- Distutils-SIG 訊息
摘要
本 PEP 規範了 Python 軟體套件應如何指定其構建依賴項,以便執行其選定的構建系統。作為此規範的一部分,引入了一種新的設定檔供軟體套件用來指定其構建依賴項(預期未來將使用相同的設定檔來處理其他設定細節)。
原理
當 Python 最初為專案開發軟體發行版的構建工具時,distutils [1] 是當時的解決方案。隨著時間推移,setuptools [2] 因在 distutils 之上增加了某些功能而流行起來。兩者都使用了 setup.py 檔案的概念,專案維護者執行該檔案以構建軟體發行版(使用者也透過執行它來安裝該發行版)。
在 distutils 下使用可執行檔案來指定構建需求並非問題,因為 distutils 是 Python 標準函式庫的一部分。將構建工具作為 Python 的一部分意味著 setup.py 沒有專案維護者需要擔心的外部依賴,即可構建其專案發行版。因為唯一的依賴項就是 Python 本身,所以無需指定任何依賴資訊。
但當專案選擇使用 setuptools 時,使用像 setup.py 這樣的可執行檔案就成了問題。如果不了解其依賴項,你就無法執行 setup.py 檔案,但目前沒有標準的方法可以在不執行該檔案的情況下,以自動化方式得知這些依賴項是什麼(資訊正是儲存在該檔案中)。這是一個死循環:除非執行該檔案,否則無法得知其內容;但若不知道其內容,該檔案便無法執行。
Setuptools 曾嘗試透過其 setup() 函式的 setup_requires 參數來解決這個問題 [3]。該解決方案存在許多問題,例如:
- 沒有工具(除了 setuptools 本身)可以在不執行
setup.py的情況下存取此資訊,但若未安裝這些項目,setup.py便無法執行。 - 雖然 setuptools 本身會安裝在此處列出的任何項目,但它們直到執行
setup()函式「期間」才會被安裝。這意味著實際使用此處新增的任何項目的唯一方法,是透過日益複雜的機制,將這些模組的匯入和使用延遲到setup()函式執行過程的後段。 - 這無法包含
setuptools本身,也不能包含setuptools的替代品,這意味著像numpy.distutils這樣的專案很大程度上無法使用它,且專案無法在使用者自然升級至較新版本的 setuptools 前利用較新的 setuptools 功能。 - 每當你執行
setup.py時,setup_requires中列出的項目就會被隱式安裝,但執行setup.py的常見方式之一是透過另一個已經在管理依賴項的工具(例如pip)。這意味著像pip install spam這樣的指令可能會導致 pip 和 setuptools 同時下載並安裝套件,且終端使用者需要同時設定「兩個」工具(且在無法控制setuptools呼叫方式的情況下)來更改設定,例如從哪個儲存庫進行安裝。這也意味著使用者需要了解兩個工具的發現規則,因為其中一個可能會支援不同的套件格式或以不同方式決定最新版本。
這導致了一種情況:setup_requires 的使用非常罕見,專案傾向於只是在 setup.py 檔案之間複製貼上程式碼片段,或者完全放棄使用它,轉而在其他地方記錄使用者在嘗試構建或安裝專案之前需要手動安裝哪些內容。
所有這些導致 pip [4] 在執行 setup.py 檔案時只能簡單地假設需要 setuptools。然而,這樣做的問題是,如果另一個專案像 setuptools 一樣開始在社群中獲得關注,它無法擴展。同時,由於 pip 無法推斷出除了 setuptools 之外還需要其他東西,導致使用者使用該專案時會遇到困難,進而阻礙了其他專案獲得關注。
本 PEP 試圖透過在特定檔案中以宣告式的方式列出專案構建系統的最小依賴項,來糾正這種情況。這允許專案列出其從原始碼檢查(checkout)到 wheel 所需的構建依賴項,同時不會陷入 setup.py 所具備的死循環陷阱,即工具無法推斷專案構建自身所需的內容。實作本 PEP 將允許專案提前指定它們依賴的構建系統,以便像 pip 這樣的工具可以確保在執行構建系統以構建專案之前安裝好這些依賴項。
為了提供本 PEP 更多的背景和動機,請思考產生專案構建產物所需的(粗略)步驟:
- 專案的原始碼檢查。
- 構建系統的安裝。
- 執行構建系統。
本 PEP 涵蓋步驟 #2。PEP 517 涵蓋步驟 #3,包括如何讓構建系統動態指定其執行工作所需的更多依賴項。然而,本 PEP 的目的是為構建系統指定能夠開始執行的最小需求集。
規範
檔案格式
構建系統依賴項將儲存在名為 pyproject.toml 的檔案中,該檔案採用 TOML 格式編寫 [6]。
選擇此格式是因為它易於人類閱讀(不像 JSON [7]),它足夠靈活(不像 configparser [9]),源自於標準(也不像 configparser [9]),並且不至於過於複雜(不像 YAML [8])。TOML 格式已被 Rust 社群作為其 Cargo 套件管理器的一部分使用 [14],且在私人電子郵件中表示他們對選擇 TOML 非常滿意。關於為何未選擇其他替代方案的更詳盡討論,可以在其他檔案格式章節中閱讀。儘管如此,作者確實意識到配置檔案格式的選擇最終是主觀的,必須做出選擇,而作者在此情況下更偏好 TOML。
我們在下方列出了工具預期要辨識/遵守的表格。本 PEP 未指定的表格保留給未來其他 PEP 使用。
build-system 表格
[build-system] 表格用於儲存與構建相關的資料。最初,該表格中只有一個鍵有效且為強制性:requires。此鍵的值必須是一個字串列表,代表執行構建系統所需的 PEP 508 依賴項(目前這意味著執行 setup.py 檔案所需的依賴項)。
對於絕大多數依賴 setuptools 的 Python 專案,pyproject.toml 檔案將是:
[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools"] # PEP 508 specifications.
由於目前 setuptools 在社群中的使用非常廣泛,預期當 pyproject.toml 檔案不存在時,構建工具將使用上述範例配置檔案作為其預設語義。
工具不應要求必須存在 [build-system] 表格。pyproject.toml 檔案可用於儲存與構建無關的配置細節,因此合法地缺少 [build-system] 表格。如果檔案存在但缺少 [build-system] 表格,則應使用上述指定的預設值。如果指定了該表格但缺少必要的欄位,則工具應將其視為錯誤。
tool 表格
[tool] 表格是任何與你的 Python 專案相關的工具(不僅僅是構建工具)可以讓使用者指定配置資料的地方,前提是他們在 [tool] 中使用子表格,例如 flit 工具會將其配置儲存在 [tool.flit] 中。
我們需要某種機制來分配 tool.* 命名空間內的名稱,以確保不同的專案不會嘗試使用相同的子表格而發生衝突。我們的規則是,專案僅在擁有 Cheeseshop/PyPI 中 $NAME 的條目時,才能使用子表格 tool.$NAME。
JSON Schema
為了僅用於說明目的而提供來自 TOML 檔案所得資料的型別特定表示,以下 JSON Schema [15] 將符合該資料格式:
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"build-system": {
"type": "object",
"additionalProperties": false,
"properties": {
"requires": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["requires"]
},
"tool": {
"type": "object"
}
}
}
否決的想法
語義版本鍵
為了對配置檔案的結構進行未來驗證,最初提議了一個 semantics-version 鍵。預設為 1,其想法是如果之前定義的鍵或表格發生了任何不向後相容的語義變更,則 semantics-version 將增加一個新數字。
然而最終決定這是一個過早的優化。預期配置檔案中語義預定義內容的變更將會相當保守。而在發生不向後相容變更的情況下,可以使用不同的名稱來表示新的語義,以避免破壞舊的工具。
更深層的巢狀命名空間
本 PEP 的早期草稿有一個頂層的 [package] 表格。其想法是為語義版本控制方案強加一些範圍限制(參見語義版本鍵以了解為什麼該想法被拒絕)。隨著範圍限制需求的消除,擁有頂層表格的意義變得多餘。
其他表格名稱
另一個為 [build-system] 表格提議的名稱是 [build]。替代名稱更短,但不能傳達出該表格中儲存資訊意圖的足夠多資訊。在 distutils-sig 郵寄清單上投票後,目前的名稱勝出。
其他檔案格式
還有其他幾種檔案格式被提出討論,但都因各種原因被拒絕。關鍵需求是該格式應可由人類編輯,並具有一個可輕易被專案整合的實作。這直接排除了某些如 XML 這樣對人類不友善且從未被嚴肅討論的格式。
所考慮檔案格式的概述
拒絕其他考量方案的關鍵原因總結於以下章節中,而完整審查(包括支援 TOML 的正面論點)可參見 [16]。
TOML 最終被選中,因為它提供了我們感興趣的所有功能,同時避免了替代方案帶來的缺點。
| 功能 | TOML | YAML | JSON | CFG/INI |
|---|---|---|---|---|
| 定義明確 | 是 | 是 | 是 | |
| 真實資料型別 | 是 | 是 | 是 | |
| 可靠的 Unicode | 是 | 是 | 是 | |
| 可靠的註解 | 是 | 是 | ||
| 人類易於編輯 | 是 | ?? | ?? | |
| 工具易於編輯 | 是 | ?? | 是 | ?? |
| 標準函式庫中 | 是 | 是 | ||
| pip 易於整合 | 是 | 不適用 | 不適用 |
(表格中的“??”表示大多數人傾向於回答“是”的項目,但實際上由於缺乏明確的規範,或者底層檔案格式規範出奇地複雜,導致在實作中出現了許多怪癖和邊緣情況)
pytoml TOML 解析器約有 300 行純 Python 程式碼,因此不在標準函式庫中並未對其造成重大不利影響。
Python 字面值也被作為潛在格式進行了討論,但未納入檔案格式審查(因為它們不是通用的預先存在檔案格式)。
JSON
JSON 格式 [7] 最初被考慮但很快被拒絕。雖然它作為人類可讀、基於字串的資料交換格式很棒,但其語法並不適合人類輕鬆編輯(例如,語法比必要情況下更冗長,且不允許註解)。
建議資料的 JSON 檔案範例如下:
{
"build": {
"requires": [
"setuptools",
"wheel>=0.27"
]
}
}
YAML
YAML 格式 [8] 的設計目標是成為 JSON [7] 的超集,同時更易於手動操作。YAML 主要存在三個問題。
一是規格龐大:如果用信紙大小的紙張列印,長達 86 頁。這留下了某人可能使用某個在一個解析器上有效但在另一個解析器上無效的 YAML 功能的可能性。有人建議標準化為一個子集,但這基本上意味著要為此檔案建立一個新的標準,這在長期看來是不可行的。
二是 YAML 本身在預設情況下是不安全的。該規範允許任意程式碼執行,這在處理配置資料時最好避免。當然,避免這種行為是有可能的——例如,PyYAML 提供了 safe_load 操作——但如果任何工具粗心地使用 load,它們就給任意程式碼執行敞開了大門。雖然本 PEP 專注於專案的構建,而這本身就涉及程式碼執行,但其他配置資料(如專案名稱和版本號)未來可能會出現在同一個檔案中,其中不需要任意程式碼執行。
最後,最受歡迎的 Python YAML 實作是 PyYAML [10],這是一個擁有數千行程式碼和可選 C 擴充模組的大型專案。雖然這本身不一定是一個問題,但這對於像 pip 這樣的專案來說就成了問題,因為他們極有可能需要將 PyYAML 作為依賴項整合以完全獨立(否則你會導致你的安裝工具需要另一個安裝工具才能工作)。已經進行了一項關於 PyYAML 的概念驗證,以查看潛在整合該函式庫的簡化版本有多容易,結果顯示這是可能的。
YAML 檔案範例如下:
build:
requires:
- setuptools
- wheel>=0.27
configparser
基於 configparser [9] 所接受內容的 INI 風格配置檔案也進行了考慮。遺憾的是,沒有關於 configparser 接受什麼的規範,導致不同版本間的支援存在差異。例如,Python 2.7 中的 ConfigParser 接受的內容與 Python 3 中的 configparser 接受的內容不同。雖然人們可以標準化 Python 3 接受的內容並簡單地整合 configparser 模組的向後移植版本,但這確實意味著本 PEP 必須明文規定所有希望使用本 PEP 所指定元資料的專案都必須使用該向後移植版本。這過於限制,如果有人不知道預期使用特定版本的 configparser,可能會導致混淆。
INI 檔案範例如下:
[build]
requires =
setuptools
wheel>=0.27
Python 字面值
有人提議使用 Python 字面值作為配置格式。該檔案將在頂層包含一個字典,資料全部位於該字典內部,區段由鍵定義。所有 Python 程式設計師都會習慣這種格式,隱含地沒有讀取配置資料的第三方依賴,並且如果由 ast.literal_eval() [13] 解析,它是安全的。Python 字面值可以與 JSON 相同,並具有支援尾隨逗號和註解的額外好處。此外,Python 更豐富的資料模型對於某些未來的配置需求可能很有用(例如,非字串字典鍵、浮點數與整數值)。
另一方面,Python 字面值是 Python 特有的格式,預計這些資料可能需要被非 Python 編寫的封裝工具等讀取。
建議資料的 Python 字面值檔案範例如下:
# The build configuration
{"build": {"requires": ["setuptools",
"wheel>=0.27", # note the trailing comma
# "numpy>=1.10" # a commented out data line
]
# and here is an arbitrary comment.
}
}
堅持使用 setup.cfg
setuptools 用作通用格式的 setup.cfg 存在兩個問題。一是它們是 .ini 檔案,正如上述 configparser 討論中所提到的那樣存在問題。另一個是該檔案的結構從未被嚴格定義,因此不知道未來使用哪種格式是安全的,而不會潛在地混淆 setuptools 的安裝。
其他檔案名稱
還有其他幾種檔案名稱被考慮並拒絕(儘管這在很大程度上是一個瑣碎的爭論話題,所以決定主要歸結為品味)。
- pysettings.toml
- 最合理的替代方案。
- pypa.toml
- 雖然引用 PyPA [11] 是合理的,但這是一個有些小眾的術語。最好讓檔案名稱在沒有特定領域知識的情況下也能說得通。
- pybuild.toml
- 從本 PEP 的限制角度來看,此檔名是有意義的,但如果任何非構建元資料被新增到檔案中,那麼這個名稱就不再有意義了。
- pip.toml
- 太工具特定了。
- meta.toml
- 太通用;專案可能希望擁有自己的元資料檔案。
- setup.toml
- 雖然由於
setup.py的存在而保持了傳統,但它不一定符合該檔案未來可能包含的內容(例如,知道專案名稱是否天生就是其設定的一部分?)。 - pymeta.toml
- 對於程式設計和/或 Python 的新手來說不直觀。
- pypackage.toml & pypackaging.toml
- 關於什麼是「套件」的名稱混淆(專案與命名空間)。
- pydevelop.toml
- 該檔案可能包含與開發無關的細節。
- pysource.toml
- 與原始碼沒有直接關係。
- pytools.toml
- 誤導,因為該檔案(目前)旨在用於專案管理。
- dstufft.toml
- 太個人化了。 ;)
參考文獻
版權
此文件已歸入公有領域 (public domain)。
來源: https://github.com/python/peps/blob/main/peps/pep-0518.rst
最後修改: 2025-09-22 12:21:58 GMT