Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 516 – pip/conda 等建置系統的抽象化

作者:
Robert Collins <rbtcollins at hp.com>,Nathaniel J. Smith <njs at pobox.com>
BDFL-Delegate:
Alyssa Coghlan <ncoghlan at gmail.com>
討論於:
Distutils-SIG 郵件列表
狀態:
已否決 (Rejected)
類型:
標準軌跡 (Standards Track)
主題:
套件封裝 (Packaging)
建立日期:
2015年10月26日
決議:
Distutils-SIG 訊息

目錄

摘要

本 PEP 規定了一個程式化介面,供 pip [1] 及其他發佈或安裝工具在處理 Python 原始碼樹(包括開發者樹,例如 git 樹,以及原始碼發佈包)時使用。

此程式化介面基於兩個主要原因,允許 pip 與其目前對 setuptools [2] 的硬性依賴關係解耦

  1. 它使得新的建置系統能夠更容易使用,而無需讓它們看起來像是 setuptools。
  2. 它促進了 setuptools 自身改變其使用者介面,而不會破壞 pip,從而實現更鬆散的耦合。

允許 pip 安裝建置系統所需的介面,也使 pip 能夠安裝套件的建置時需求,這是讓 pip 與 easy-install 的安裝組件實現完整功能對等的重要一步。

由於 PEP 426 仍是草案,我們無法使用其定義的中繼資料格式。然而,PEP 427 的 wheel 格式廣泛使用且規範良好,因此我們採用了其中的 METADATA 格式來指定發佈依賴項和通用專案中繼資料。PEP 508 提供了一種描述依賴項的獨立語言,我們將其封裝在一個輕量級的 JSON 綱要中,以描述引導依賴項。

由於 PEP 314 中指定的 Python sdist 也是原始碼樹,本 PEP 正在更新 sdist 的定義。

PEP 已拒絕

本 PEP 中提出的基於 CLI 的方法已被拒絕,取而代之的是 PEP 517 中提出的基於 Python API 的方法。用於與作為獨立子進程執行的建置後端進行通訊的具體 CLI 將被視為前端開發工具實現的實作細節。

動機

在 Python 套件生態系統中,對於建置系統和 pip 之間目前的鎖定狀況存在著大量積壓的挫折感。打破這種鎖定對於 pip、setuptools 以及其他建置系統(如 flit [3])都是更好的。

規範

概述

建置工具將透過讀取原始碼樹根目錄下的 pypa.json 檔案來定位。該檔案描述了如何取得建置工具以及執行該工具的命令名稱。

所有工具都將被期望符合單一的命令列介面,該介面以 pip 目前使用 setuptools 的 setup.py 介面為模型。

pypa.json

檔案 pypa.json 作為 pip 和其他希望建置原始碼樹的工具的通用設定檔,供其查詢配置。Python 原始碼樹中若不存在 pypa.json 檔案,則意味著該專案使用 setuptools 或與 setuptools 相容的建置系統。

此 JSON 具有以下綱要。額外的鍵會被忽略,這允許 pypa.json 作為其他相關工具的設定檔。如果這樣做,所選的鍵必須在 tools 下命名空間。

{"tools": {"flit": ["Flits content here"]}}
綱要
綱要版本。本 PEP 定義版本「1」。若不存在則預設為「1」。所有讀取此檔案的工具在遇到無法辨識的綱要版本時必須報錯。
bootstrap_requires(引導需求)
可選的 PEP 508 依賴規範列表,這些規範必須在執行建置工具之前安裝。例如,如果使用 flit,則需求可能是
bootstrap_requires: ["flit"]
build_command(建置命令)
一個必需的鍵,這是一個 Python 格式字串列表 [8],描述要執行的命令。例如,如果使用 flit,則建置命令可能是
build_command: ["flit"]

如果使用一個可執行的模組命令 fred

build_command: ["{PYTHON}", "-m", "fred"]

程序介面

要執行的命令由一個簡單的 Python 格式字串 [8] 定義。

這允許具有專用腳本的建置系統,以及那些使用「python -m somemodule」調用的建置系統。

程序將在當前工作目錄設定為原始碼樹根目錄的情況下執行。

執行時,程序不應從標準輸入 (stdin) 讀取——儘管 pip 目前執行建置系統時將其 stdin 連接到自己的 stdin,但標準輸出 (stdout) 和標準錯誤 (stderr) 會被重定向,無法與使用者進行通訊。

與一般程序相同,非零的退出狀態表示錯誤。

可用的格式變數

PYTHON
正在使用的 Python 解譯器。這對於啟用呼叫僅為 Python 進入點的事物至關重要。
{PYTHON} -m foo

可用的環境變數

這些變數由建置系統的呼叫者設定,並且將始終可用。

PATH
標準系統路徑。
PYTHON
與格式變數相同。
PYTHONPATH
用於根據正常的 Python 機制控制 sys.path。

子命令

建置系統必須支援多個獨立的子命令。以下範例使用 flit 作為 build_command 進行說明。

build_requires(建置需求)
查詢建置需求。建置需求以 UTF-8 編碼的 JSON 文件返回,其中包含一個鍵 build_requires,該鍵由一個 PEP 508 依賴規範列表組成。額外的鍵必須被忽略。build_requires 命令是唯一一個無需設定建置環境即可執行的命令。

範例命令

flit build_requires
metadata(中繼資料)
查詢專案中繼資料。中繼資料且僅中繼資料應以 UTF-8 編碼輸出到標準輸出 (stdout)。pip 只會執行 metadata 命令一次,以確定需要下載和安裝哪些其他套件。中繼資料將按照 PEP 427 的規定以 wheel METADATA 檔案格式輸出。

請注意,由 metadata 命令產生的中繼資料,以及在生成的 wheel 中存在的中繼資料必須是相同的。

範例命令

flit metadata
wheel -d OUTPUT_DIR
用於建置專案 wheel 的命令。OUTPUT_DIR 將指向一個現有目錄,wheel 應輸出到該目錄。標準輸出 (stdout) 和標準錯誤 (stderr) 沒有語義意義。只應輸出一個檔案——如果輸出多個,則 pip 會任意選取一個來使用。

範例命令

flit wheel -d /tmp/pip-build_1234
develop [–prefix PREFIX]
用於執行專案的「開發」模式原地安裝的命令。標準輸出 (stdout) 和標準錯誤 (stderr) 沒有語義意義。

並非所有建置系統都能執行開發模式安裝。如果建置系統無法執行開發模式安裝,則在執行時應報錯。請注意,這樣會導致像 pip install -e foo 這樣的使用操作失敗。

prefix 選項用於定義安裝的替代前綴。雖然 setuptools 有 --root--user 選項,但它們可以透過 --prefix 等效完成,而 pip 或其他接受 --root--user 選項的工具應進行適當的轉換。

root 選項用於定義命令應在其內部操作的替代根目錄。

例如

flit develop --root /tmp/ --prefix /usr/local

應該將腳本安裝在 /tmp/usr/local/bin 內,即使正在使用的 Python 環境報告 sys.prefix 為 /usr/,這會導致使用 /tmp/usr/bin/。類似的邏輯也適用於套件檔案等。

建置環境

除了 build_requires 命令之外,所有命令都在建置環境中執行。不要求特定的實作,但建置環境必須滿足以下要求。

  1. 專案的 build_requires 所指定的所有依賴項都必須能夠從 $PYTHON 內部匯入。
  1. 建置所需套件提供的所有命令列腳本都必須存在於 $PATH 中。

由此推論,建置系統不能假設可以存取任何未宣告為 build_requires 或不在 Python 標準函式庫中的 Python 套件。

密封式建置

本規範並未規定建置是否應該是密封式的。現有的建置工具(例如 setuptools)將使用已安裝的建置時需求版本(例如 setuptools_scm),並且只在版本衝突或缺少依賴項時安裝其他版本。然而,透過始終隔離建置並僅使用指定的依賴項,很可能會建立更好的一致性。

然而,這裡存在一些微妙的問題——例如使用者如何強制避免一個滿足某些套件依賴項但卻是有問題的建置需求版本。未來的 PEP 可能會解決這個問題,但目前不在本規範範圍內——它不影響建置系統與需要執行建置的事物之間協調所需的中繼資料,因此不屬於 PEP 的內容。

升級

「pypa.json」是版本化的,以允許未來更改而無需相容性要求。

在新 PEP 中升級任一綱要的順序將是

  1. 發布新的 PEP,定義更新的綱要。如果該綱要不能完全向後相容,則必須定義一個新的版本號。
  2. 消費者(例如 pip)實現對新綱要版本的支援。
  3. 套件作者在樂意引入對支援新綱要版本的「pip」(以及潛在的其他消費者)版本的依賴時,選擇採用新綱要。

本 PEP 的初次部署也將遵循相同的流程:——無需 setuptools 墊片即可使用本 PEP 功能的傳播,將主要取決於第一個支援它的 pip 版本的採用率。

sdist 中的靜態中繼資料

本 PEP 並未解決目前無法信任 sdist 中靜態中繼資料的問題。這與識別和使用原始碼樹中正在使用的建置系統(無論它是否來自 sdist)是獨立的問題。

編譯器選項的處理

處理不同的編譯器選項不在本規範的範圍內。

pip 目前透過在執行 setuptools 時將使用者提供的字串附加到其執行的命令列來處理編譯器選項。這種方法足以與本 PEP 中定義的建置系統介面配合使用,但例外情況是,隨著不同建置系統的演進,全域指定的選項將不再全域生效。這個問題可以在 pip(或 conda 或其他安裝程式)中解決,而不會影響互通性。

從長遠來看,wheel 應該能夠表達使用不同編譯器或選項建置的 wheel 之間的差異,而這屬於 PEP 的內容。

範例

一個使用 flit 的「pypa.json」範例

{"bootstrap_requires": ["flit"],
 "build_command": "flit"}

當「pip」讀取此內容時,它會在嘗試使用 flit 之前,準備一個包含 flit 的環境。

由於 flit 目前不支援 setup-requires,flit build_requires 只會輸出一個常數字串

{"build_requires": []}

flit metadata 將查詢 flit.ini 並將中繼資料整理成 wheel METADATA 檔案,然後輸出到標準輸出 (stdout)。

flit wheel 將需要接受一個 -d 參數,該參數指示它將 wheel 輸出到何處(pip 需要這個)。

回溯相容性

較舊版本的 pip 將仍然無法處理替代的建置系統。這與現狀相比沒有更糟——並且個別建置系統專案可以決定是否包含一個墊片 setup.py

所有現有的能夠產生 wheel 並執行開發模式安裝的建置系統都應該能夠在此抽象下運行,並且只需要為它們建構一個特定的轉接器並發佈到 PyPI 上。

在缺少 pypa.json 檔案的情況下,像 pip 這樣的工具應假定為 setuptools 建置系統,並直接使用 setuptools 命令。

網絡效應

採用與 setuptools 不相容的建置系統的專案——即它們沒有 setup.py,或者 setup.py 不接受現有工具嘗試使用的命令——將無法被這些現有工具安裝。

如果這些專案被其他專案使用,這個影響將會層層蔓延。

特別是,由於 pip 目前不處理 setup-requires,任何採用與 setuptools 不相容的建置系統的專案 (A),如果被尚未轉換為擁有 pypa.json 的第二個專案 (B) 作為 setup-requirement 消費,將會導致任何版本的 pip 都無法安裝 B。這是因為當 pip 運行 'setup.py egg_info' 時,B 中的 setup.py 將觸發 easy-install,而 easy-install 將嘗試安裝 A 但失敗。

因此,我們建議目前被用作 setup-requires 的工具,要么確保它們保留一個 setuptools 墊片,要么找到其消費者並讓他們在自己遷移之前,全部升級到使用 pypa.json。實際上這是不可能的,所以建議是無限期地保留 setuptools 墊片——對於 pbr、setuptools_scm 以及 numpy 等專案都是如此。

setuptools 墊片

可以編寫一個通用的 setuptools 墊片,它看起來像 setup.py,但在內部使用 pypa.json 來驅動建置。這對於 pip 使用該系統來說並非必需,但將允許套件作者使用新功能,同時仍保持與舊版本 pip 的相容性。

原理

本 PEP 最初始於 distutils-sig 上的一個長篇郵件列表討論 [6]。此後舉行了一次線上會議,以釐清所有人的立場。會議記錄已發布到該列表 [7]

本規範是將在那裡達成的共識轉化為 PEP 形式,同時對一些次要的剩餘問題做出了一些任意選擇。

設計的基本啟發式方法是專注於引入抽象,而不要求與抽象沒有嚴格相關的開發。在改進空間不大,或者使用現有介面的成本非常高的情況下,我們將改進視為依賴項,否則將其推遲到未來的迭代中。

我們選擇 wheel METADATA 檔案而不是定義一個新規範,因為 pip 已經可以處理 wheel 的 .dist-info 目錄,這些目錄將所有必要的資料編碼在 METADATA 檔案中。PEP 426 無法使用,因為它仍是草案,而定義一個新的中繼資料格式雖然我們應該這樣做,但這是另一個獨立的問題。在磁碟上使用目錄不會為介面增加任何價值(由於 setuptools CLI 的限制,pip 今天就必須這樣做)。

使用「develop」作為命令是因為目前沒有 PEP 指定執行「setuptools develop」功能的事物之間的互通性——因此我們需要先定義它,然後 pip 才能承擔執行「develop」步驟的責任。一旦完成,我們就可以發布本 PEP 的後續 PEP。

使用命令列 API 而非 Python API 有點爭議。從根本上說,任何東西都可以使其運作,而 pip 維護者強烈主張保留基於進程的介面——這在今天的 pip 中是成熟且穩健的。

選擇 JSON 作為檔案格式是在多個限制之間進行的折衷。首先,標準函式庫 (stdlib) 中沒有 YAML 解譯器,也沒有其他低摩擦結構化檔案格式的解譯器。其次,INIParser 因多種原因而是一種糟糕的格式,主要是它結構非常簡單——但 pip 的維護者不喜歡它。JSON 包含在 stdlib 中,具有足夠的結構,允許未來嵌入任何我們想要的內容,而無需嵌入式 DSL。

Donald 建議使用 setup.cfg 和現有的 setuptools 命令列,而不是發明新東西。雖然這可以讓互通性變得不那麼明顯,但它在 pip 方面需要幾乎同樣多的工程工作——在 setup.cfg 中尋找新鍵,實現非安裝環境以運行建置。而其他建置系統作者不希望透過提供看起來像 setuptools 但行為卻大相徑庭的東西來混淆他們的使用者,這似乎比 pip 學習如何調用自訂建置工具更是一個大問題。

metadata 和 wheel 命令需要具有一致的中繼資料,以避免可能發生的競爭條件:pip 讀取中繼資料並根據其操作,然後生成的 wheel 卻具有不相容的需求。這種競爭目前被使用 PEP 426 環境標記的套件所利用,以與不支援環境標記的舊版本 pip 配合使用。本 PEP 不需要這種利用,因為要么使用 setuptools 墊片(與舊版本 pip 配合),要么使用支援環境標記的 pip。setuptools 墊片可以處理舊版本 pip 所需的差異利用。

我們討論了是否要有一個 sdist 動詞。這樣做的主要原因是確保建置系統能夠產生 pip 可以建置的 sdist——但這是循環的:本 PEP 的重點是讓 pip 能夠可靠地消費此類 sdist 或 VCS 原始碼樹,而無需 setuptools 的實作。能夠從現有的原始碼樹建立新的 sdist 並非 pip 今天所做的事情,雖然有一個 PR 旨在作為從原始碼建置的一部分來實現這一點,但它存在爭議且缺乏共識。我們不對所有建置系統強加要求,而是將其視為 YAGNI (You Ain't Gonna Need It),如果需要,我們將在介面的未來版本中添加這樣的動詞。現有的 PEP 314 對 sdist 的要求仍然適用,distutils 或 setuptools 使用者可以使用 setup.py sdist 來建立 sdist。其他工具應建立與 PEP 314 相容的 sdist。請注意,pip 本身不要求 PEP 314 相容性——它不使用 sdist 中的任何中繼資料——它們被視為來自磁碟或版本控制的原始碼樹。

參考文獻


來源: https://github.com/python/peps/blob/main/peps/pep-0516.rst

最後修改: 2025年02月01日 08:55:40 GMT