Python依赖性管理的迷宫

477 阅读7分钟

Python依赖性管理的迷宫

在这篇文章中,我想对Python中的依赖性管理做一些说明。Python的依赖性管理是一个完全不同的世界。

20多年来,我一直在为JVM开发代码,先是用Java,然后是用Kotlin。然而,JVM并不是银弹,例如,在脚本中:

  1. 虚拟机会产生额外的内存需求
  2. 在很多情况下,脚本的运行时间不够长,无法获得性能上的任何好处。字节码是被解释的,永远不会被编译成本地代码。

由于这些原因,我现在用Python写我的脚本。其中一个收集来自不同来源的社交媒体指标,并将其存储在BigQuery中进行分析。

我不是一个Python开发者,但我正在学习--通过艰苦的方式。在这篇文章中,我想对Python中的依赖性管理做一些说明。

Python中足够的依赖性管理

在 JVM 上,依赖性管理似乎是一个已解决的问题。首先,你选择你的构建工具,最好是 Maven 或其他我不知道名字的工具。然后,你声明你的直接依赖,工具会管理间接依赖。这并不意味着没有问题,但你可以或多或少地解决这些问题。

Python的依赖性管理是一个完全不同的世界。首先,在Python中,运行时和它的依赖关系是全系统的。一个系统只有一个运行时,而依赖是在这个系统的所有项目中共享的。因为这是不可行的,所以在开始一个新项目时,首先要做的是创建一个虚拟环境。

解决这个问题的方法是创建一个虚拟环境,一个自成一体的目录树,它包含一个特定版本的Python安装,再加上一些额外的包。

然后不同的应用程序可以使用不同的虚拟环境。为了解决前面的需求冲突的例子,应用程序 A 可以有自己的安装了 1.0 版本的虚拟环境,而应用程序 B 有另一个安装了 2.0 版本的虚拟环境。如果应用程序B要求一个库升级到3.0版本,这将不会影响应用程序A的环境。

--虚拟环境和软件包

一旦这样做了,事情就开始认真了。

Python 提供了一个叫做pip 的依赖性管理工具,开箱即用。

你可以使用一个叫做 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 依赖关系来检查依赖关系树。

Shell

pip install deptree
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

它的内容如下:Flask 需要Werkzeug ,而 又需要MarkupSafeWerkzeugMarkupSafe 有资格作为我的项目的反相依赖。

版本部分也很有趣。第一部分提到了已安装的版本,而注释部分提到了兼容的版本范围。例如,Jinja 需要3.0 或以上的版本,而安装的版本是3.1.2

安装的版本是pip 在安装时发现的最新兼容版本。pipdeptree 在随每个库分发的setup.py 文件中知道兼容性。

设置脚本是使用Distutils构建、分发和安装模块的所有活动的中心。设置脚本的主要目的是向Distutils描述你的模块分布,以便对你的模块进行操作的各种命令能做正确的事情。

--编写设置脚本

这里是Flask的。

Python

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"],
    },
)

Pip 和过渡性依赖

问题的出现是因为我希望我的依赖关系是最新的。为此,我配置了 Dependabot 来观察requirements.txt 中列出的依赖关系的新版本。当此类事件发生时,它会在我的 repo 中打开一个PR。大多数时候,PR工作得很顺利,但在少数情况下,我在合并后运行脚本时出现了错误。它看起来像下面这样。

纯文本

ERROR: libfoo 1.0.0 has requirement libbar<2.5,>=2.0, but you'll have libbar 2.5 which is incompatible.

问题是,Dependabot为列出的每个库打开了一个PR。但是一个新的库的版本可能会被发布,而这个版本超出了兼容性的范围。

想象一下下面的情况。我的项目需要libfoo 这个依赖。反过来,libfoo 需要libbar 依赖关系。在安装时,pip 使用最新版本的libfoo 和最新兼容版本的libbar 。由此产生的requirements.txt 是。

纯文本

libfoo==1.0.0
libbar==2.0

一切都按预期进行。一段时间后,Dependabot运行并发现libbar 已经发布了一个新版本,例如2.5 。忠实地,它打开了一个PR,以合并以下变化。

纯文本

libfoo==1.0.0
libbar==2.5

上述问题是否出现,完全取决于libfoo 1.0.0setup.py 中是如何指定其依赖关系的。如果2.5 属于兼容范围,它就能工作;如果不属于,它就不能。

pip-compile 拯救

pip 的问题是,它列出了横向的依赖关系和直接的依赖关系。Dependabot随后获取了所有依赖关系的最新版本,但并没有验证交叉依赖关系的版本更新是否在该范围内。它有可能进行检查,但requirements.txt 文件格式没有结构化:它没有区分直接依赖和横向依赖。明显的解决方案是只列出直接依赖关系。

好消息是,pip 只允许列出直接依赖关系;它自动安装了横向依赖关系。坏消息是,我们现在有两个requirements.txt ,没有办法区分它们:有的只列出直接依赖关系,有的列出所有的依赖关系。

这就需要一个替代方案。pip-tools有一个。

  1. 一个是在requirements.in 文件中列出它们的直接依赖关系,其格式与requirements.txt
  2. pip-compile 工具会从requirements.in 中生成一个requirements.txt

例如,鉴于我们的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 ,它是兼容的。