PEP 650 – 為 Python 專案指定安裝程式要求
- 作者:
- Vikram Jayanthi <vikramjayanthi at google.com>, Dustin Ingram <di at python.org>, Brett Cannon <brett at python.org>
- 討論至:
- Discourse 帖子
- 狀態:
- 已撤回
- 型別:
- 標準跟蹤
- 主題:
- 打包
- 建立日期:
- 2020年7月16日
- 釋出歷史:
- 2021年1月14日
摘要
Python 包安裝程式之間並不完全可互操作。雖然 pip 是使用最廣泛的安裝程式和事實上的標準,但像 Poetry 或 Pipenv 這樣的其他安裝程式也很受歡迎,因為它們提供了獨特的功能,這些功能最適合某些工作流程,並且不直接符合 pip 的工作方式。
雖然安裝程式選項的豐富性對有特定需求的終端使用者來說是好事,但它們之間缺乏互操作性使得支援所有潛在安裝程式變得困難。特別是,缺乏用於宣告依賴關係的標準化需求檔案意味著每個工具都必須顯式使用才能安裝以其各自格式指定的依賴關係。否則,工具必須發出需求檔案,這會導致安裝程式丟失潛在資訊,並增加開發者工作流程中的匯出步驟。
透過提供一個可用於呼叫相容安裝程式的標準化 API,我們可以解決這個問題,而無需解決不同安裝程式及其鎖定檔案之間的個體問題、獨特要求和不相容性。
實現該規範的安裝程式可以以統一的方式呼叫,使使用者能夠像直接呼叫一樣使用他們選擇的安裝程式。
術語
- 安裝程式介面
- 安裝程式後端和通用安裝程式互動的介面。
- 通用安裝程式
- 一個透過呼叫安裝程式介面的可選呼叫方法來呼叫安裝程式後端的安裝程式。這也可以被認為是安裝程式前端,類似於 build 專案用於 PEP 517。
- 安裝程式後端
- 實現安裝程式介面的安裝程式,允許它被通用安裝程式呼叫。安裝程式後端也可以是通用安裝程式,但這不是必需的。與 PEP 517 相比,這將是 Flit。安裝程式後端可以是包裝某個後端安裝程式的包裝器包,例如,Poetry可以選擇不支援此 API,但一個包可以作為包裝器呼叫 Poetry,以適當地使用 Poetry 來執行安裝。
- 依賴組
- 一組相關聯的依賴項,需要同時安裝才能達到某種目的。例如,“test”依賴組可能包括執行測試套件所需的依賴項。依賴組的指定方式取決於安裝程式後端。
動機
該規範允許任何人呼叫和互動實現指定介面的安裝程式後端,從而在現有工具特定的安裝過程之上提供一個普遍支援的層。
反過來,這將使得所有實現指定介面的安裝程式都可以在支援單個通用安裝程式的環境中使用,只要該安裝程式也實現了此規範。
下面,我們確定了適用於 Python 社群利益相關者以及任何與 Python 包安裝程式互動的人的各種用例。對於開發者或公司而言,此 PEP 將為 Python 包安裝程式帶來增強的功能和靈活性。
提供者
提供者是指提供與 Python 打包及後續 Python 包安裝程式互動的服務或軟體工具的各方(組織、個人、社群等)。考慮了兩種不同型別的提供者
平臺/基礎設施提供者
平臺提供者(雲環境、應用託管等)和基礎設施服務提供者需要支援包安裝程式,以便其使用者可以安裝 Python 依賴項。大多數只支援 pip,但使用者對其他 Python 安裝程式有需求。大多數提供者不想維護對一個以上安裝程式的支援,因為它增加了其軟體或服務的複雜性並消耗了大量資源。
透過此規範,我們可以使提供者支援的通用安裝程式能夠呼叫使用者期望的安裝程式後端,而無需提供者的平臺具有對該後端的特定知識。這意味著如果 Poetry 實現此 PEP 提出的安裝程式後端 API(或某個包裝 Poetry 以提供 API 的其他包),那麼平臺提供者將隱含地支援 Poetry。
IDE提供者
整合開發環境可能會與 Python 包的安裝和管理進行互動。大多數只支援 pip 作為 Python 包安裝程式,使用者需要找到變通方法來使用其他包安裝程式安裝他們的依賴項。與 PaaS & IaaS 提供者的情況類似,IDE 提供者不想維護對 N 個不同 Python 安裝程式的支援。相反,IDE 可以透過充當通用安裝程式來呼叫安裝程式介面的實現者(安裝程式後端)。
開發者
開發者是指編寫程式碼並使用 Python 包安裝程式和 Python 包的團隊、個人或社群。考慮了三種不同型別的開發者
使用 PaaS & IaaS 提供者的開發者
大多數 PaaS 和 IaaS 提供者只支援一個 Python 包安裝程式:pip。(一些例外情況包括 Heroku 的 Python buildpack,它支援 pip 和 Pipenv)。這決定了開發者在使用這些提供者時可以使用的安裝程式,而這可能不是他們應用程式或工作流程的最佳選擇。
採用此 PEP 以成為安裝程式後端的安裝程式將允許使用者使用第三方平臺/基礎設施,而無需擔心他們必須使用哪個 Python 包安裝程式,只要提供者使用通用安裝程式。
使用 IDE 的開發者
大多數 IDE 只支援 pip 或少數幾個 Python 包安裝程式。因此,如果開發者使用不受支援的包安裝程式,他們必須使用變通方法或 hacky 方法來安裝其依賴項。
如果 IDE 使用/提供通用安裝程式,它將允許使用開發者想要的任何安裝程式後端來安裝依賴項,從而使他們不必費力去安裝依賴項,以便更緊密地整合到 IDE 的工作流程中。
與其他開發者合作的開發者
開發者希望在與其他開發者合作時使用他們選擇的安裝程式,但目前不得不同步他們的安裝程式選擇以相容依賴項的安裝。如果所有首選安裝程式都實現了指定介面,那將允許跨安裝程式的使用,從而讓開發者能夠選擇一個安裝程式,而無需考慮其協作者的偏好。
升級者 & 包基礎設施提供者
包升級者和 CI/CD 中的包基礎設施(如 Dependabot、PyUP 等)目前支援少數安裝程式。它們透過直接解析和編輯特定於安裝程式的依賴檔案(如 requirements.txt 或 poetry.lock)以及相關的包資訊(如升級、降級或新雜湊)來工作。與平臺和 IDE 提供者類似,這些提供者中的大多數都不希望支援 N 個不同的 Python 包安裝程式,因為這將需要支援 N 種不同的檔案型別。
目前,這些服務/機器人必須單獨實現對每個包安裝程式的支援。不可避免的是,最受歡迎的安裝程式會先得到支援,而不那麼受歡迎的工具通常永遠不會得到支援。透過實現此規範,這些服務/機器人可以支援任何(相容的)安裝程式,允許使用者選擇他們想要的工具。這將允許在該領域進行更多創新,因為平臺和 IDE 不再被迫過早地選擇“贏家”。
開源社群
指定安裝程式要求並採用此 PEP 將減少 Python 包安裝程式與人們工作流程之間的摩擦。因此,它將減少 Python 包安裝程式與 PaaS 或 IDE 等第三方基礎設施/技術之間的摩擦。總而言之,它將使 Python 專案的開發、部署和維護更加容易,因為 Python 包的安裝將變得更簡單、更具互操作性。
指定要求並建立安裝程式介面也可以加快圍繞安裝程式的創新速度。這將允許安裝程式進行實驗並新增獨特的功能,而無需生態系統的其餘部分也這樣做。支援新安裝程式將變得更容易,並且可能性更大,無論它增加了什麼功能以及以何種格式寫入依賴項,同時減少了完成此工作所需的開發者時間和資源。
規範
與 PEP 517 指定構建系統的方式類似,安裝系統資訊將儲存在 pyproject.toml 檔案中的 install-system 表中。
[install-system]
install-system 表用於儲存與安裝系統相關的資料和資訊。此表有幾個必需的鍵:requires 和 install-backend。requires 鍵包含安裝程式後端執行所需的最低要求,並且將由通用安裝程式安裝。install-backend 鍵包含安裝後端入口點的名稱。這將允許通用安裝程式安裝安裝程式後端本身執行所需的依賴項(而不是安裝程式後端將要安裝的依賴項),並呼叫安裝程式後端。
如果任一必需鍵缺失或為空,則通用安裝程式應引發錯誤。
與此介面互動的所有包名稱都假定遵循 PEP 508 的“Python 軟體包的依賴項規範”格式。
一個示例 install-system 表
#pyproject.toml
[install-system]
#Eg : pipenv
requires = ["pipenv"]
install-backend = "pipenv.api:main"
安裝程式要求
由 requires 鍵指定的依賴項必須在 PEP 517 指定的約束範圍內。特別是,不允許依賴迴圈,並且如果檢測到迴圈,通用安裝程式應拒絕安裝依賴項。
其他引數或工具特定資料
其他引數或工具(安裝程式後端)資料也可以儲存在 pyproject.toml 檔案中。這將在 PEP 518 指定的“tool.*”表中。例如,如果安裝程式後端是 Poetry 並且您想指定多個依賴組,則 tool.poetry 表可以如下所示
[tool.poetry.dev-dependencies]
dependencies = "dev"
[tool.poetry.deploy]
dependencies = "deploy"
資料也可以根據安裝程式後端的需要以其他方式儲存(例如,單獨的配置檔案)。
安裝程式介面
安裝程式介面包含強制性鉤子和可選鉤子。相容的安裝程式後端必須實現強制性鉤子,並可以實現可選鉤子。通用安裝程式可以自己實現任何安裝程式後端鉤子,充當通用安裝程式和安裝程式後端,但這並非必需。
所有鉤子都接受 **kwargs 任意引數,這些引數是安裝程式後端可能需要但尚未指定的,允許向後相容。如果傳遞了意外的引數給安裝程式後端,它應該忽略它們。
以下資訊類似於 PEP 517 中對應的部分。鉤子可能會使用關鍵字引數呼叫,因此實現它們的安裝程式後端應該小心確保它們的簽名與上面引數的順序和名稱都匹配。
所有鉤子都可以將任意資訊性文字列印到 stdout 和 stderr。它們不得從 stdin 讀取,並且通用安裝程式可以在呼叫鉤子之前關閉 stdin。
通用安裝程式可以捕獲後端的 stdout 和/或 stderr。如果後端檢測到輸出流不是終端/控制檯(例如,不是 sys.stdout.isatty()),它應確保它寫入該流的任何輸出都經過 UTF-8 編碼。如果捕獲的輸出不是有效的 UTF-8,通用安裝程式不得失敗,但它可能無法保留該情況下的所有資訊(例如,它可能使用 Python 中的 replace 錯誤處理程式進行解碼)。如果輸出流是終端,安裝程式後端負責準確地呈現其輸出,就像在終端中執行的任何程式一樣。
如果鉤子丟擲異常或導致程序終止,則表示出現錯誤。
強制性鉤子
invoke_install
安裝依賴項
def invoke_install(
path: Union[str, bytes, PathLike[str]],
*,
dependency_group: str = None,
**kwargs
) -> int:
...
path: 一個絕對路徑,安裝程式後端應該從中呼叫(例如,pyproject.toml所在的目錄)。dependency_group: 一個可選標誌,指定安裝程式後端應安裝的依賴組。如果依賴組不存在,安裝將失敗。使用者可以透過呼叫get_dependency_groups()來查詢所有依賴組,前提是安裝程式後端支援依賴組。**kwargs: 安裝程式後端可能需要但尚未指定的任意引數,允許向後相容。- 返回 : 退出程式碼(int)。成功時為 0,不成功時為任何正整數。
通用安裝程式將使用退出程式碼來確定安裝是否成功,並且應自行返回退出程式碼。
可選鉤子
invoke_uninstall
解除安裝指定的依賴項
def invoke_uninstall(
path: Union[str, bytes, PathLike[str]],
*,
dependency_group: str = None,
**kwargs
) -> int:
...
path: 一個絕對路徑,安裝程式後端應該從中呼叫(例如,pyproject.toml所在的目錄)。dependency_group: 一個可選標誌,指定安裝程式後端應解除安裝的依賴組。**kwargs: 安裝程式後端可能需要但尚未指定的任意引數,允許向後相容。- 返回 : 退出程式碼(int)。成功時為 0,不成功時為任何正整數。
通用安裝程式必須在通用安裝程式自身被呼叫時所在的同一路徑呼叫安裝程式後端。
通用安裝程式將使用退出程式碼來確定解除安裝是否成功,並且應自行返回退出程式碼。
get_dependencies_to_install
返回 invoke_install(...) 將要安裝的依賴項。這允許包升級者(例如,Dependabot)在不解析依賴檔案的情況下檢索嘗試安裝的依賴項。
def get_dependencies_to_install(
path: Union[str, bytes, PathLike[str]],
*,
dependency_group: str = None,
**kwargs
) -> Sequence[str]:
...
path: 一個絕對路徑,安裝程式後端應該從中呼叫(例如,pyproject.toml所在的目錄)。dependency_group: 指定一個依賴組,以獲取invoke_install(...)將為該依賴組安裝的依賴項。**kwargs: 安裝程式後端可能需要但尚未指定的任意引數,允許向後相容。- 返回: 要安裝的依賴項列表(PEP 508 字串)。
如果指定了組,安裝程式後端必須返回與提供的依賴組對應的依賴項。如果指定的組不存在,或者安裝程式後端不支援依賴組,安裝程式後端必須引發錯誤。
如果未指定組,並且安裝程式後端提供了預設/未指定組的概念,安裝程式後端可以返回預設/未指定組的依賴項,但否則必須引發錯誤。
get_dependency_groups
返回可安裝的依賴組。這允許通用安裝程式列舉安裝程式後端已知的所有依賴組。
def get_dependency_groups(
path: Union[str, bytes, PathLike[str]],
**kwargs
) -> AbstractSet[str]:
...
path: 一個絕對路徑,安裝程式後端應該從中呼叫(例如,pyproject.toml所在的目錄)。**kwargs: 安裝程式後端可能需要但尚未指定的任意引數,允許向後相容。- 返回: 已知依賴組的集合,以字串形式表示。空集表示沒有依賴組。
update_dependencies
根據輸入的包列表輸出依賴檔案
def update_dependencies(
path: Union[str, bytes, PathLike[str]],
dependency_specifiers: Iterable[str],
*,
dependency_group=None,
**kwargs
) -> int:
...
path: 一個絕對路徑,安裝程式後端應該從中呼叫(例如,pyproject.toml所在的目錄)。dependency_specifiers: 一個可迭代的依賴項,以 PEP 508 字串形式表示,例如:["requests==2.8.1", ...]。可選地,為特定的依賴組。dependency_group: 包列表所屬的依賴組。**kwargs: 安裝程式後端可能需要但尚未指定的任意引數,允許向後相容。- 返回 : 退出程式碼(int)。成功時為 0,不成功時為任何正整數。
示例
讓我們考慮實現一個使用 pip 及其需求檔案作為依賴組的安裝程式後端。實現可能(非常粗略地)如下所示
import subprocess
import sys
def invoke_install(path, *, dependency_group=None, **kwargs):
try:
return subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"-r",
dependency_group or "requirements.txt",
],
cwd=path,
).returncode
except subprocess.CalledProcessError as e:
return e.returncode
如果我們稱這個包為 pep650pip,那麼我們可以在 pyproject.toml 中指定
[install-system]
#Eg : pipenv
requires = ["pep650pip", "pip"]
install-backend = "pep650pip:main"
基本原理
所有鉤子都接受 **kwargs,以允許向後相容,並允許工具特定的安裝程式後端功能,這需要使用者提供鉤子未要求的附加資訊。
雖然安裝程式後端必須是 Python 包,但它們在被呼叫時做什麼是一個實現細節。例如,安裝程式後端可以充當平臺包管理器(例如 apt)的包裝器。
該介面絕不試圖指定安裝程式後端應如何工作。這是故意的,以便允許安裝程式後端進行創新並以自己的方式解決問題。這也意味著此 PEP 對作業系統打包不持立場,因為那將是安裝程式後端的領域。
在 Python 中定義 API 確實意味著最終需要執行一些 Python 程式碼。但這並不排除使用非 Python安裝程式後端(例如 mamba),因為它們可以作為 Python 程式碼的子程序執行。
向後相容性
此 PEP 對預先存在的程式碼和功能沒有影響,因為它只為通用安裝程式添加了新功能。任何現有的安裝程式都應保持其現有的功能和用例,因此沒有向後相容性問題。只有旨在利用此新功能的程式碼才會有動力去更改其預先存在的程式碼。
安全隱患
惡意的使用者在使用標準化的安裝程式規範方面沒有任何能力或更容易的訪問許可權。可以透過此 PEP 指定的介面由通用安裝程式呼叫的安裝程式將由使用者明確宣告。如果使用者選擇了惡意的安裝程式,那麼使用通用安裝程式呼叫它與使用者直接呼叫該安裝程式沒有區別。作為安裝程式後端的惡意安裝程式不會獲得額外的許可權或能力。
被拒絕的想法
標準化的鎖定檔案
標準化的鎖定檔案將解決許多與指定安裝程式要求相同的問題。例如,它將允許 PaaS/IaaS 只支援一個能夠讀取標準化鎖定檔案(無論由哪個安裝程式建立)的安裝程式。標準化鎖定檔案的問題在於 Python 包安裝程式之間的需求差異以及透過鎖定檔案建立可重複環境的基本問題(這是主要好處之一)。
安裝程式之間的需求和依賴項檔案中儲存的資訊差異很大,並且依賴於安裝程式的功能。例如,像 Poetry 這樣的 Python 包安裝程式需要所有 Python 版本和平臺的資訊,並計算適當的雜湊,而 pip 則不需要。此外,pip 不能保證重現相同的環境(安裝完全相同的依賴項),因為這超出了其功能範圍。這使得標準化鎖定檔案更難實現,並使得使鎖定檔案特定於工具似乎更合適。
讓安裝程式後端支援建立虛擬環境
由於安裝程式後端很可能具有虛擬環境的概念以及如何安裝到其中,因此曾簡要考慮過讓它們也支援建立虛擬環境。但最終,這被認為是一個正交的想法。
未解決的問題
dependency_group 引數是否應該接受一個可迭代物件?
這將允許在一次呼叫中指定不重疊的依賴組,例如,“docs”和“test”組,它們具有獨立的依賴項,但開發者在開發時可能希望同時安裝它們。
安裝程式後端是否在程序內執行?
如果安裝程式後端在程序內執行,那麼它大大簡化了知道要為/安裝到哪個環境的問題,因為可以查詢即時 Python 環境以獲取相應的資訊。
在程序外執行允許最小化被安裝環境與安裝程式後端(以及可能的通用安裝程式)之間發生衝突的潛在問題。
強制要求由提議的介面產生的結果能夠饋入其他部分嗎?
例如,get_dependencies_to_install() 和 get_dependency_groups() 的結果可以傳遞給 invoke_install()。這將防止所提出介面的各個部分的結果之間出現漂移,但它使得介面的更多部分成為必需的,而不是可選的。
失敗條件時丟擲異常而不是退出程式碼
有人建議 API 應該丟擲異常而不是返回退出程式碼。如果您將此 PEP 視為幫助將當前安裝程式轉換為安裝程式後端,那麼依賴退出程式碼是有意義的。還有一點是 API 沒有特定的返回值,因此傳遞退出程式碼不會干擾函式的返回值。
與之對比的是在出錯時丟擲異常。這可能提供一種更結構化的錯誤丟擲方法,儘管為了能夠捕獲錯誤,將需要將異常型別作為介面的一部分來指定。
版權
本文件置於公共領域或 CC0-1.0-Universal 許可證下,以更寬鬆者為準。
來源:https://github.com/python/peps/blob/main/peps/pep-0650.rst