在这篇文章中,我想对 Python 中的依赖管理有所了解。Python 依赖管理是一个完全不同的世界。
20 多年来,我一直在为 JVM 开发代码,首先是 Java,然后是 Kotlin。但是,JVM 并不是灵丹妙药,例如,在脚本中:
- 虚拟机需要额外的内存
- 在许多情况下,脚本运行的时间不够长,无法在性能方面获得任何好处。字节码被解释并且永远不会编译为本机代码。
由于这些原因,我现在用 Python 编写脚本。其中之一从不同来源收集社交媒体指标并将其存储在 BigQuery 中以供分析。
我不是 Python 开发人员,但我正在学习 - 很难。在这篇文章中,我想对 Python 中的依赖管理有所了解。
Python 中足够的依赖管理
在 JVM 上,依赖管理似乎是一个已解决的问题。首先,您选择您的构建工具,最好是 Maven 或另一种我不应该命名的工具。然后,您声明您的直接依赖关系,并且该工具管理间接依赖关系。这并不意味着没有陷阱,但您可以或多或少地快速解决它们。
Python 依赖管理是一个完全不同的世界。首先,在 Python 中,运行时及其依赖项是系统范围的。一个系统只有一个运行时,并且依赖项在该系统上的所有项目之间共享。因为不可行,所以开始一个新项目的第一件事就是创建一个虚拟环境。
这个问题的解决方案是创建一个虚拟环境,一个自包含的目录树,其中包含特定版本的 Python 的 Python 安装,以及一些额外的包。
然后不同的应用程序可以使用不同的虚拟环境。为了解决前面的冲突需求示例,应用程序 A 可以拥有自己的安装了 1.0 版的虚拟环境,而应用程序 B 拥有另一个安装了 2.0 版的虚拟环境。如果应用程序 B 需要将库升级到版本 3.0,这不会影响应用程序 A 的环境。
--虚拟环境和包
一旦完成,事情就会认真开始。
pipPython 提供了一个开箱即用的依赖管理工具:
您可以使用名为 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.3Flask==2.2.2itsdangerous==2.1.2Jinja2==3.1.2MarkupSafe==2.1.1Werkzeug==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 setupsetup( 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有一个:
- 一个在一个文件中列出了它们的直接依赖关系,该requirements.in文件的格式与requirements.txt
- 该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 flaskflask==2.2.2 # via -r requirements.initsdangerous==2.1.2 # via flaskjinja2==3.1.2 # via flaskmarkupsafe==2.1.1 # via # jinja2 # werkzeugwerkzeug==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是兼容的。