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

Python 增強提案 (Python Enhancement Proposals)

PEP 766 – 在多個索引之間進行明確的優先順序選擇

作者:
Michael Sarahan <msarahan at gmail.com>
贊助人:
Barry Warsaw <barry at python.org>
PEP 委託人:
Paul Moore <p.f.moore at gmail.com>
討論於:
Discourse 討論串
狀態:
草案
類型:
資訊類
主題:
套件封裝 (Packaging)
建立日期:
2024年11月18日
公告歷史:
2024年11月18日

目錄

摘要

套件解析是 Python 使用者體驗中的一個關鍵部分,因為它是擴展 Python 核心功能的手段。套件解析的體驗在大多數情況下都被視為理所當然,直到有人遇到套件安裝程式執行了預期之外的操作。多個索引的安裝程式行為一直是產生意外行為的常見來源。由於 pip 的普及,它長期以來定義了生態系統中其他工具所預期的標準行為,但 Python 安裝程式在如何處理多個索引方面正出現分歧。這種分歧的核心在於:是在解析發行版之前合併所有索引的內容,還是按順序逐個處理每個索引。pip 在匹配發行版之前會合併所有索引,而 uv 則在轉向下一個索引之前先在一個索引上匹配發行版。每一種方法都有其優缺點。本 PEP 旨在描述這些行為(分別稱為「版本優先」和「索引優先」),以便社群討論和故障排除時能擁有共同的詞彙,並讓工具能夠根據這些描述實作可預測的行為。

動機

Python 套件使用者經常需要指定 PyPI 以外的索引或套件來源。外部索引的存在原因有很多:

在大多數情況下,完全放棄 PyPI 並非理想做法。相反,使用者通常希望 PyPI 仍作為套件來源,但優先順序較低。遺憾的是,pip 目前的設計排除了這種優先順序概念。一些 Python 安裝工具已經開發出處理多個索引的替代方法,其中包括表達索引優先順序的機制,例如 uvPDM

創新與客製化的潛力固然令人興奮,但它也存在進一步分散 Python 套件生態系統的風險,而這已被認為是 Python 的弱點之一。本 PEP 的動機是鼓勵安裝程式提供更多關於它們如何處理多個索引的見解,並提供一個可供整個社群通用的詞彙表。

規範

「版本優先」(Version priority)

這種行為的特徵是安裝程式總是獲取套件的「最佳」版本,而不管它來自哪個索引。「最佳」由安裝程式的最佳化演算法定義,該演算法權衡套件的各種特性,同時考量使用者輸入(例如偏好僅安裝二進位檔,或不使用二進位檔)。雖然不同安裝程式的最佳化標準和使用者選項可能有所不同,但所有「版本優先」安裝程式共有的通用特徵是,索引內容會在候選項目選擇之前進行整理(collated)。

當所有配置的索引都同樣被信任,且在發行版可互換性假設方面表現良好時,「版本優先」最為有用。鏡像站點在這方面表現特別好。這種可互換性假設使得比較給定套件的發行版變得有意義。如果沒有它,安裝程式就不再是「蘋果比蘋果」。在實務上,不同的索引通常會擁有與其他索引內容不同的檔案,例如針對特殊硬體的建置,或同一套件的不同元數據。在這些情況下,「版本優先」行為可能會導致不可預期的、不理想的結果,而這正是使用者通常尋求某種索引優先順序的原因。此外,當索引之間的信任度存在差異時,「版本優先」無法提供一種偏好更受信任索引的方法。這已被依賴混淆攻擊(dependency confusion attacks)所利用,而 PEP 708 被提議作為將信任外部索引的概念硬編碼到索引中的一種方式。

「版本優先」是一個新名稱,引入新術語應始終保持謹慎。本 PEP 參考了 uv 專案,該專案將其實作版本優先行為的方式稱為「unsafe-best-match」。這裡的命名確實很難。一方面,稱 pip 的預設行為本質上是「不安全的」並不準確。加入可能存在惡意的索引才是導致對此行為擔憂的原因。PEP 708 增加了一種限制安裝程式從意外、潛在不安全索引獲取套件的方法。另一方面,「最佳匹配」(best-match)一詞在技術上正確,但也容易誤導。「最佳匹配」因使用者和應用程式而異。從上述匹配標準來看,它是「全域最佳」的,但在使用者眼中不一定是「最佳」的。「版本優先」是一個建議術語,它避免了 uv 術語帶來的擔憂,同時以使用者識別套件比較方式的最直觀方式近似了該行為。

「索引優先」(Index priority)

在索引優先順序中,解析器會一次一個地為每個索引尋找候選項目。只有在當前套件請求沒有可行的候選項目時,解析器才會進入後續索引。索引優先順序不會將索引組合成一個全域的、扁平的命名空間。由於索引是按順序搜尋的,來自較早索引的套件將被優先於來自較晚索引的套件,無論後者是否在安裝程式的最佳化標準下具有更好的匹配。對於給定的安裝程式,最佳化標準和選擇演算法在索引優先順序和版本優先順序中應保持一致。唯一的區別在於對多個索引的處理:版本優先是合併處理,索引優先則是個別處理。

索引的指定順序決定了它們在搜尋過程中的優先順序。因此,安裝程式載入索引配置的方式必須是可預測且可重現的。本 PEP 沒有規定任何特定的機制,只是說安裝程式應提供一種對其來源集合進行排序的方法。安裝程式理想情況下也應提供可選的除錯輸出,以提供關於正在考慮哪個索引的見解。

每個套件的尋找器(finder)應從索引列表的開頭開始,因此每個套件都會從索引列表重新開始。換句話說,如果某個套件在第一個索引上沒有有效的候選項目,但在第二個索引上找到了,那麼後續的套件仍應從第一個索引開始搜尋,而不是直接從第二個開始。

索引優先順序策略暗示了一個理想的行為,即不會出現「驚喜」更新,也就是說,低優先順序索引上的版本升級不會勝過受控、已批准的高優先順序索引。這與 PEP 708 的安全性改進有關,套件可以限制發行版來源的外部索引,但索引優先順序對終端使用者而言更具可配置性。預期只有在高優先順序索引或索引優先順序配置發生變更時,套件安裝才會發生變化。這種穩定性和可預測性使得將索引配置為環境的持續性屬性,而不是安裝指令的一次性參數,變得更具可行性。

快取鍵(Cache keys)

由於索引優先順序承認給定套件在不同索引中可能具有不同內容的可能性,快取和鎖定檔案(lockfiles)現在應包含下載發行版的來源索引。如果沒有這一點,在更改配置的索引列表後,快取或鎖定檔案可能會提供來自低優先順序索引的同名發行版。如果每個索引都遵循建議的行為(即針對給定檔名在不同索引間提供相同的檔案),這不是問題。然而,該建議並不容易強制執行,將來源索引添加到快取鍵中將是一個明智的防禦性變更。

請求失效並轉向低優先順序索引的方式

  • 高優先順序索引中根本不存在該套件名稱
  • 來自高優先順序索引的所有發行版因版本指定符、相容的 Python 版本、平台標籤、撤回(yanked)或其他原因而被過濾掉
  • 安裝程式的黑名單配置指定在給定索引上應忽略特定的套件名稱
  • 高優先順序索引無法存取(例如防火牆規則封鎖、維護中暫時無法使用、其他雜項臨時網路問題)。這是一個不太明確的細節,應由使用者控制。一方面,這種行為會導致不可預測、可能無法重現的結果,因為它會意外地落入低優先順序索引。另一方面,優雅的回退對某些使用者可能更有價值,特別是如果他們能安全地假設所有索引都同樣值得信任。pip 目前的行為是優雅回退:如果索引有連線問題,你會看到警告,但安裝會繼續使用任何其他可用的索引。由於索引優先順序可以在索引之間傳達不同的信任等級,實作索引優先順序的安裝程式在遇到網路問題時,應預設引發錯誤並中止。安裝程式可以選擇提供一個旗標,允許在網路錯誤時轉向低優先順序索引。

在給定索引內的處理遵循現有行為,但會在一個索引的邊界處停止,只有在耗盡該索引內的所有優先順序偏好後,才會進入下一個索引。這意味著統一套件集合中的現有優先順序在轉向較低優先順序索引之前,會分別應用於每個索引。

在最佳化標準的每個層面上都存在取捨:

  • 版本:索引優先順序會使用高優先順序索引中的舊版本,即使另一個索引上有更新的版本。
  • wheel 與 sdist:安裝程式是否應該先使用高優先順序索引中的 sdist,再嘗試低優先順序索引中的 wheel?
  • 更具體的平台 wheel 優於通用 wheel:安裝程式是否應該先使用高優先順序索引中通用性較低的 wheel,再使用低優先順序索引中更具體的 wheel?
  • 諸如 pip 的 --prefer-binary 之類的旗標:安裝程式是否應該先使用高優先順序索引中的 sdist,再考慮低優先順序索引中的 wheel?

安裝程式可以自由地以不同方式實作這些優先順序,但應記錄其最佳化標準以及處理轉向低優先順序索引的方式。例如,安裝程式可以規定,除非已經遍歷所有配置的索引且未找到可安裝的二進位候選項目,否則 --prefer-binary 不應安裝 sdist。

鏡像(Mirroring)

如目前所描述的,索引優先順序方案破壞了多個索引網址提供相同內容的使用場景。這類鏡像可能用於改善網路問題或提高可靠性。安裝程式在增加索引優先順序的同時保留鏡像功能的一種方法是,增加使用者可定義的「索引組」概念,其中假定組中的每個索引都是等價的。這與 Poetry 的套件來源概念類似,不同之處在於這允許任意數量的可優先排序群組,並且假設群組成員為鏡像。在每個群組內,內容可以合併,或每個成員可以同時獲取,響應最快的索引將代表該群組。

回溯相容性

本 PEP 並未規定對任何安裝程式進行強制變更,因此只有在工具選擇採用目前所實作行為之外的索引行為時,才會引入相容性問題。

本 PEP 的語言與包括 pip 和 uv 在內的現有工具不完全一致。本 PEP 的語言可以在審查期間更改,或者如果偏好本 PEP 的語言,其他專案可以與之接軌。提出這些術語的唯一目標是建立一個核心、通用的詞彙表,使用戶更容易了解其他安裝程式。

由於某些工具依賴其中一種行為,可能會出現一些潛在問題,即針對特定行為量身打造的資源/套件可能會損害那些依賴另一種行為的使用者的體驗。

  • 不同的索引可能有不同的元數據。例如,不能假設索引「A」上套件「something」的元數據與索引「B」上的「something」具有相同的依賴項。這打破了版本優先順序的基本假設,但索引優先順序可以處理此問題。當安裝程式在搜尋順序中轉向低優先順序索引時,這意味著從新索引刷新套件元數據。這既是改進也是複雜化。說它複雜是因為快取的元數據條目必須同時由套件名稱和索引網址進行索引(key),而不僅僅是套件名稱。說它是一種改進是因為,只要套件的發行版分佈在不同的索引中,套件的不同實作變體就可以擁有不同的依賴項。
  • 使用索引優先順序時,使用者可能無法獲得預期的更新,因為某些高優先順序索引尚未更新/與 PyPI 同步以獲取最新套件。如果高優先順序索引有有效的候選項目,則不會找到更新的套件。這需要詳細溝通,因為它與 pip 既定的行為背道而馳。
  • 透過增加索引優先順序,安裝程式將提高所選索引的可預測性,而索引託管者可能會濫用這一點,提供同名但內容不同的檔案。在版本優先順序中,這違反了關鍵的套件可互換性假設,並會導致混亂。索引優先順序將更具可操作性,但這種情況仍然具有巨大的混亂潛力。開發能支援安裝程式識別這些混亂問題的工具將會很有幫助。這些工具可以獨立於安裝程式程序運作,作為驗證一組索引是否合理的手段。根據這些工具的時間成本,安裝程式可以將其作為程序的一部分執行。當然,使用者可以自行承擔忽略這些建議的風險。

安全性影響

索引優先順序為使用者建立了一種機制,可以明確指定其索引之間的信任層級。因此,它限制了依賴混淆攻擊的潛力。PEP 708 曾拒絕將索引優先順序作為依賴混淆攻擊的解決方案。本 PEP 要求重新考慮該拒絕,因為索引優先順序服務於不同的目的。本 PEP 的主要動機是支援實作變體,這是另一場希望促成 PEP 的討論的主題。它與 PEP 708 並不互斥,也不建議撤銷或收回 PEP 708。它是對於如何讓我們允許使用者以比「每次安裝」更細粒度的層級選擇要使用的索引這一問題的回答。

關於 PEP 708 拒絕索引優先順序的更深入討論,請參閱本 PEP 的 discuss.python.org 討論串

如何教學

起初,目標並非改變 pip 或任何其他工具的預設優先順序行為。教學的最佳方式也許是觀察留言板、GitHub 問題追蹤器和聊天頻道,留意那些索引優先順序有助於解決的問題。幾項長期討論已經存在,這將是廣告宣傳這些概念的好地方。這兩種官方支援的行為主題需要文件,我們這些 PEP 的作者將在審查期間開發這些文件。這些文件可能包含對多個索引的補充,並將安裝程式之間的概念進行交叉連結。至少,我們期望新增到 PyPUGpip 的文件中。

安裝程式宣傳其運作的行為將非常重要,特別是在錯誤訊息中,這將提供向使用者提供有關這些行為資源的方式。

uv 使用者已經在體驗索引優先順序。uv 妥善地記錄了此行為,但始終有可能提高該文件在命令列中的可發現性,也就是使用者實際上會遇到意外行為的地方

參考實作

uv 專案以其預設行為展示了索引優先順序。不過,uv 是用 Rust 實作的,因此如果需要基於 Python 工具的參考實作,我們這些 PEP 的作者將提供一個。特別對於 pip,我們看到的實作計畫如下:

  • 對於不使用 --extra-index-url--find-links 的使用者,將不會有任何改變,也不需要遷移。
  • pip 使用者將能夠透過 CLI 和 pip.conf 中的新設定來選擇索引優先順序行為。本建議不建議將任何策略作為任何安裝程式的預設值。它僅建議記錄工具提供的策略。
  • 為任何使用多於一個索引的 pip 操作啟用額外的 info 等級輸出。在此輸出中,註明目前的策略設定、隱含行為的簡要總結,以及描述不同選項的文件連結。
  • 新增除錯輸出,詳細識別每個步驟中使用的索引,包括檔案在配置層級中的位置,以及它被包含的位置(透過設定檔、環境變數或 CLI 旗標)。
  • 在整個 pip 安裝過程中追蹤哪個套件/發行版使用了哪個索引。儲存此資訊,使其可供 pip freeze 等工具使用。
  • 補充 PEP 751(鎖定檔案),記錄套件/發行版的來源索引。

否決的想法

  • 建議使用者設定代理/鏡像,例如 devpiArtifactory,如果存在則提供本地檔案,若無匹配的本地檔案則轉發到另一個伺服器(PyPI)。

    這與本提案的行為非常接近,只是此方法需要託管伺服器,且在某些環境中可能無法存取或無法進行配置。同樣重要的是要考慮到,對於營運自己索引的組織(例如為了克服 PyPI 大小限制)來說,這並不能解決終端使用者對 --extra-index-url 或代理/鏡像的需求。也就是說,除非組織代理/鏡像整個 PyPI,並讓使用者將其代理/鏡像配置為唯一的索引,否則組織無法從此方法中獲得任何改進。

  • 建置標籤(build tags)和/或本地版本指定符(local version specifiers)是否足夠?

    建置標籤和本地版本指定符將優先於沒有這些標籤和/或指定符的套件。在一個套件池中,託管在 PyPI 以外伺服器上的這些建置版本將優先於 PyPI 上的套件(PyPI 很少使用建置標籤,並禁止本地版本指定符)。當套件提供者希望提供自己的本地覆蓋時,這種方法是可行的,例如 為使用者提供優化建置的 HPC 維護人員。在某些方面它不太可行,例如建置標籤不會出現在 pip freeze 元數據中,且 PyPI 上不允許本地版本指定符。建置和維護具有本地建置標籤變體的套件集合也需要大量工作。

    https://discuss.python.org/t/dependency-notation-including-the-index-url/5659/21

  • 那麼 PEP 708 呢?這難道還不夠嗎?

    PEP 708 旨在專門解決依賴混淆攻擊,並未解決索引間存在實作變體的可能性。它是一種過濾外部 URL 並在索引元數據中為外部索引編碼允許清單的方法。它並沒有改變目前頻道之間缺乏優先順序或偏好的現狀。

  • 命名空間(Namespacing)

    命名空間是一種指定套件的方法,使得套件的 Python 使用方式不變,但套件的安裝會限制套件的來源。PEP 752 最近提議了一種透過保留前綴作為分組元素,在扁平套件命名空間(如 PyPI)中多工處理套件所有者的方法。NPM 的「範圍」(scopes)概念已被提出作為另一個很好的範例。本 PEP 的不同之處在於它針對的是多個索引,而不是扁平的套件命名空間。在可預測地選擇特定套件來源方面,淨效應大致相同,不同之處在於命名空間方法更依賴於用這些命名空間前綴來命名套件,而本 PEP 粒度較小,會抓取使用者指定的所有更高優先順序索引中的套件。命名空間方法依賴於所有配置的索引以類似方式處理給定的命名空間,這留下了通常的擔憂,即並非所有配置的索引都同樣受信任。命名空間的想法與本 PEP 並不衝突,但它也沒有像本 PEP 那樣改善對索引信任的表達方式。

待決問題

[任何仍在決定/討論中的點。]

致謝

這項工作得到了 NVIDIA 的經濟支持(透過僱用作者)。NVIDIA 的同事透過他們的投入顯著改進了本 PEP。Astral Software 開創了索引優先順序的行為,從而為本文件奠定了基礎。pip 作者對於版本優先順序行為的始終如一的方向和耐心溝通值得讚揚,特別是在面對具爭議的安全擔憂時。


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

最後修改:2024-11-21 20:00:24 GMT