天天看點

Python 依賴管理的迷宮

作者:qaseven

在這篇文章中,我想對 Python 中的依賴管理有所了解。Python 依賴管理是一個完全不同的世界。

Python 依賴管理的迷宮

20 多年來,我一直在為 JVM 開發代碼,首先是 Java,然後是 Kotlin。但是,JVM 并不是靈丹妙藥,例如,在腳本中:

  1. 虛拟機需要額外的記憶體
  2. 在許多情況下,腳本運作的時間不夠長,無法在性能方面獲得任何好處。位元組碼被解釋并且永遠不會編譯為本機代碼。

由于這些原因,我現在用 Python 編寫腳本。其中之一從不同來源收集社交媒體名額并将其存儲在 BigQuery 中以供分析。

我不是 Python 開發人員,但我正在學習 - 很難。在這篇文章中,我想對 Python 中的依賴管理有所了解。

Python 中足夠的依賴管理

在 JVM 上,依賴管理似乎是一個已解決的問題。首先,您選擇您的建構工具,最好是 Maven 或另一種我不應該命名的工具。然後,您聲明您的直接依賴關系,并且該工具管理間接依賴關系。這并不意味着沒有陷阱,但您可以或多或少地快速解決它們。

Python 依賴管理是一個完全不同的世界。首先,在 Python 中,運作時及其依賴項是系統範圍的。一個系統隻有一個運作時,并且依賴項在該系統上的所有項目之間共享。因為不可行,是以開始一個新項目的第一件事就是建立一個虛拟環境。

這個問題的解決方案是建立一個虛拟環境,一個自包含的目錄樹,其中包含特定版本的 Python 的 Python 安裝,以及一些額外的包。

然後不同的應用程式可以使用不同的虛拟環境。為了解決前面的沖突需求示例,應用程式 A 可以擁有自己的安裝了 1.0 版的虛拟環境,而應用程式 B 擁有另一個安裝了 2.0 版的虛拟環境。如果應用程式 B 需要将庫更新到版本 3.0,這不會影響應用程式 A 的環境。

--虛拟環境和包

一旦完成,事情就會認真開始。

pipPython 提供了一個開箱即用的依賴管理工具:

您可以使用名為 pip 的程式安裝、更新和删除軟體包。

--使用 pip 管理包

工作流程如下:

  1. 在虛拟環境中安裝所需的依賴項:pip install flask
  2. 在安裝了所有必需的依賴項後,将它們儲存在requirements.txt按約定命名的檔案中:pip freeze > requirements.txt

    該檔案應與正常代碼一起儲存在一個人的VCS中。

  3. pip其他項目開發人員可以通過指向安裝相同的依賴項requirements.txt:pip install -r requirements.txt

以下是requirements.txt上述指令的結果:

click==8.1.3

Flask==2.2.2

itsdangerous==2.1.2

Jinja2==3.1.2

MarkupSafe==2.1.1

Werkzeug==2.2.2

依賴和傳遞依賴

在描述這個問題之前,我們需要解釋一下什麼是傳遞依賴。傳遞依賴項是項目不直接需要的依賴項,而是項目的依賴項之一或依賴項的依賴項一直需要的依賴項。在上面的示例中,我添加了flask依賴項,但pip總共安裝了 6 個依賴項。

我們可以安裝deptree依賴來檢查依賴樹。

pip install deptree

輸出如下:

Flask==2.2.2 # flask

Werkzeug==2.2.2 # Werkzeug>=2.2.2

MarkupSafe==2.1.1 # MarkupSafe>=2.1.1

Jinja2==3.1.2 # Jinja2>=3.0

MarkupSafe==2.1.1 # MarkupSafe>=2.0

itsdangerous==2.1.2 # itsdangerous>=2.0

click==8.1.3 # click>=8.0

# deptree and pip trees

它的内容如下:Flaskrequires Werkzeug,這反過來又需要MarkupSafe。Werkzeug并MarkupSafe有資格作為我的項目的傳遞依賴項。

版本部分也很有趣。第一部分提到安裝的版本,而注釋部分是指相容的版本範圍。例如Jinja需要版本3.0或以上,安裝的版本為3.1.2.

安裝的版本是安裝時找到的最新相容pip版本。pip并deptree了解setup.py沿每個庫分發的檔案的相容性:

安裝腳本是使用 Distutils 建構、分發和安裝子產品的所有活動的中心。設定腳本的主要目的是向 Distutils 描述您的子產品分發,以便對您的子產品進行操作的各種指令執行正确的操作。

--編寫安裝腳本

flask在這裡:

from setuptools import setup

setup(

name="Flask",

install_requires=[

"Werkzeug >= 2.2.2",

"Jinja2 >= 3.0",

"itsdangerous >= 2.0",

"click >= 8.0",

"importlib-metadata >= 3.6.0; python_version < '3.10'",

],

extras_require={

"async": ["asgiref >= 3.2"],

"dotenv": ["python-dotenv"],

},

)

點和傳遞依賴

出現問題是因為我希望我的依賴項是最新的。為此,我已将 Dependabot 配置為監視requirements.txt. 當這樣的事件發生時,它會在我的 repo 中打開一個PR 。大多數時候,PR 就像一個魅力,但在少數情況下,當我在合并後運作腳本時會發生錯誤。它如下所示:

純文字1錯誤:libfoo 1.0.0 要求 libbar<2.5,>=2.0,但您将擁有不相容的 libbar 2.5。

問題是 Dependabot 為列出的每個庫打開一個 PR。但是可以釋出一個新的庫版本,這超出了相容性的範圍。

想象一下下面的情況。我的項目需要libfoo依賴。反過來,libfoo需要libbar依賴。在安裝時,pip使用最新版本libfoo和最新相容版本的libbar. 結果requirements.txt是:

純文字1libfoo==1.0.02庫==2.0

一切都按預期工作。過了一會兒,Dependabot 運作并發現libbar已經釋出了一個新版本,例如2.5. 忠實地,它打開了一個 PR 來合并以下更改:

純文字1libfoo==1.0.02庫==2.5

是否出現上述問題僅取決于如何libfoo 1.0.0在setup.py. 如果2.5在相容範圍内,則有效;如果沒有,它不會。

pip-compile拯救

問題pip在于它列出了傳遞依賴和直接依賴。Dependabot 然後擷取所有依賴項的最新版本,但不驗證傳遞依賴項版本更新是否在該範圍内。它可能會檢查,但requirements.txt檔案格式不是結構化的:它不區分直接依賴和傳遞依賴。顯而易見的解決方案是僅列出直接依賴項。

好消息是pip隻允許列出直接依賴項;它自動安裝傳遞依賴。壞消息是我們現在有兩個requirements.txt選項無法區分它們:一些僅列出直接依賴關系,而另一些則列出所有依賴關系。

它需要一個替代方案。pip-tools有一個:

  1. 一個在一個檔案中列出了它們的直接依賴關系,該requirements.in檔案的格式與requirements.txt
  2. 該pip-compile工具requirements.txt從requirements.in.

例如,給定我們的 Flask 示例:

#

# This file is autogenerated by pip-compile with python 3.10

# To update, run:

#

# pip-compile requirements.in

#

click==8.1.3

# via flask

flask==2.2.2

# via -r requirements.in

itsdangerous==2.1.2

# via flask

jinja2==3.1.2

# via flask

markupsafe==2.1.1

# via

# jinja2

# werkzeug

werkzeug==2.2.2

# via flask

pip install -r requirements.txt

它具有以下好處和後果:

  • 生成的requirements.txt包含注釋以了解依賴關系樹
  • 由于pip-compile生成檔案,您不應将其儲存在 VCS 中
  • 該項目與依賴于的遺留工具相容requirements.txt
  • 最後但同樣重要的是,它改變了安裝工作流程。不是安裝包然後儲存它們,而是首先列出包然後安裝它們。

此外,Dependabot 可以管理pip-compile.

結論

這篇文章描述了預設 Python 的依賴管理系統以及它如何破壞自動版本更新。我們繼續描述pip-compile解決問題的替代方案。

請注意,Python 存在依賴管理規範,PEP 621 – 在 pyproject.toml 中存儲項目中繼資料。它類似于 Maven 的 POM,但格式不同。這在我的腳本上下文中是多餘的,因為我不需要分發項目。但是你應該這樣做,知道它pip-compile是相容的。