Python依赖性管理的迷宫
在这篇文章中,我想对Python中的依赖性管理做一些说明。Python的依赖性管理是一个完全不同的世界。
20多年来,我一直在为JVM开发代码,先是用Java,然后是用Kotlin。然而,JVM并不是银弹,例如,在脚本中:
- 虚拟机会产生额外的内存需求
- 在很多情况下,脚本的运行时间不够长,无法获得性能上的任何好处。字节码是被解释的,永远不会被编译成本地代码。
由于这些原因,我现在用Python写我的脚本。其中一个收集来自不同来源的社交媒体指标,并将其存储在BigQuery中进行分析。
我不是一个Python开发者,但我正在学习--通过艰苦的方式。在这篇文章中,我想对Python中的依赖性管理做一些说明。
Python中足够的依赖性管理
在 JVM 上,依赖性管理似乎是一个已解决的问题。首先,你选择你的构建工具,最好是 Maven 或其他我不知道名字的工具。然后,你声明你的直接依赖,工具会管理间接依赖。这并不意味着没有问题,但你可以或多或少地解决这些问题。
Python的依赖性管理是一个完全不同的世界。首先,在Python中,运行时和它的依赖关系是全系统的。一个系统只有一个运行时,而依赖是在这个系统的所有项目中共享的。因为这是不可行的,所以在开始一个新项目时,首先要做的是创建一个虚拟环境。
解决这个问题的方法是创建一个虚拟环境,一个自成一体的目录树,它包含一个特定版本的Python安装,再加上一些额外的包。
然后不同的应用程序可以使用不同的虚拟环境。为了解决前面的需求冲突的例子,应用程序 A 可以有自己的安装了 1.0 版本的虚拟环境,而应用程序 B 有另一个安装了 2.0 版本的虚拟环境。如果应用程序B要求一个库升级到3.0版本,这将不会影响应用程序A的环境。
--虚拟环境和软件包
一旦这样做了,事情就开始认真了。
Python 提供了一个叫做pip 的依赖性管理工具,开箱即用。
你可以使用一个叫做 pip 的程序来安装、升级和删除软件包。
工作流程如下:
-
一个人在虚拟环境中安装所需的依赖性。
外壳
pip install flask -
当一个人安装了所有需要的依赖性后,按照惯例将它们保存在一个名为
requirements.txt的文件中。外壳
pip freeze > requirements.txt该文件应与常规代码一起保存在自己的VCS中。
-
其他项目开发者可以通过将
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 ,而 又需要MarkupSafe 。Werkzeug 和MarkupSafe 有资格作为我的项目的反相依赖。
版本部分也很有趣。第一部分提到了已安装的版本,而注释部分提到了兼容的版本范围。例如,Jinja 需要3.0 或以上的版本,而安装的版本是3.1.2 。
安装的版本是pip 在安装时发现的最新兼容版本。pip 和deptree 在随每个库分发的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.0 在setup.py 中是如何指定其依赖关系的。如果2.5 属于兼容范围,它就能工作;如果不属于,它就不能。
pip-compile 拯救
pip 的问题是,它列出了横向的依赖关系和直接的依赖关系。Dependabot随后获取了所有依赖关系的最新版本,但并没有验证交叉依赖关系的版本更新是否在该范围内。它有可能进行检查,但requirements.txt 文件格式没有结构化:它没有区分直接依赖和横向依赖。明显的解决方案是只列出直接依赖关系。
好消息是,pip 只允许列出直接依赖关系;它自动安装了横向依赖关系。坏消息是,我们现在有两个requirements.txt ,没有办法区分它们:有的只列出直接依赖关系,有的列出所有的依赖关系。
这就需要一个替代方案。pip-tools有一个。
- 一个是在
requirements.in文件中列出它们的直接依赖关系,其格式与requirements.txt 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 ,它是兼容的。