这篇文章将会介绍 PDM 的 lock 策略,基于当前最新版本 2.13。英文版由 LLM 辅助翻译。
PDM 是如何解析依赖的?
PDM 底层是用的一个纯 Python 实现的 PubGrub 解析算法,Resolvelib。若用通俗的语言解释,它的解析过程大致如下:
- 选择一个未解析的依赖,获取它的所有版本的列表
- 从最新版本开始尝试,获取这个版本的依赖
- 检查这个版本的依赖与已解析的依赖是否有冲突
- 若有冲突,尝试下一个版本
- 若无冲突,将这个依赖的版本加入结果中
- 若尝试过所有版本都无法解析,回溯到上个确定结果的依赖,尝试下一个版本
- 最终所有依赖都解析完成,得到一个满足所有依赖的版本列表
解析完成以后,PDM 就会将结果写到 pdm.lock
文件中,这个文件除了包含所有依赖的版本信息,还包含了一些其他元数据。
条件依赖
有时我们需要根据不同的条件安装不同的包版本,这会利用 Marker,例如:
pytest >= 7.0; python_version >= "3.6"
pytest < 7.0; python_version < "3.6"
但是 PDM 现在暂时不支持解析这种条件依赖,原因是 PDM 的依赖解析器实现会把包名作为解集中的 key,换句话说,每个包在解集中有且只有一个确定的版本。不得不承认,这确实是 PDM 的一大缺陷,欢迎大家贡献代码来解决这个问题。
Lock 文件的元数据
pdm.lock
文件是一个 TOML 格式的文件,在它的 [metadata]
表中包含了一些元数据,包括:
[metadata]
groups = ["default", "all", "doc", "pytest", "test", "tox", "workflow"]
strategy = ["cross_platform", "inherit_metadata"]
lock_version = "4.4.1"
content_hash = "sha256:13270582f610302a77a5e1fef2192e1a65f5b6202cf15aedf12bf799de8de45c"
lock_version
这是一个三位的版本号,标明这个 lock 文件的兼容情况。第一位表示向后不兼容的改动,第二位表示向后兼容但向前不兼容的改动,第三位表示向前向后都兼容的改动。
比如说,假如当前 lock 文件的版本是 4.4.2,那么:
- 支持读取的版本有:
4.3.0
,4.4.0
,4.4.3
- 不能读取的版本有:
5.1.0
,3.0.0
每当更新 lock 文件时,PDM 会把当前的 lock 版本写入文件中。通过这个版本号,PDM 就可以决定是否应该尝试读取这个 lock 文件,或是提示用户重新生成 lock 文件。
groups
记录了这个 lock 文件是从哪些依赖分组生成的,列表中的每个值都对应了 pyproject.toml
中 optional-dependencies
或 dev-dependencies
的一个分组。 当依赖解析完成时,这些分组就会被记录在 lock 文件中,安装时,PDM 会检查你要求安装的分组是否包含其中。
content_hash
因为 lock 文件对应了一组初始输入,即从哪些依赖解析生成。在 PDM 中,这个输入就是 pyproject.toml
中写的依赖信息,content_hash
就是从这些内容计算出来的一个 sha256 值,当你的 pyproject.toml
发生变化,PDM 就会重新计算这个值,如果发现与 lock 文件中的值不一致,就会为你更新 lock 文件,并把新的 content_hash
写入其中。
Lock 策略
[metadata]
表中的 strategy
字段就记录了当前 lock 文件的策略,用来控制依赖解析的过程。
cross_platform
默认启用,PDM 会将所有平台的文件都写入 Lock 文件中,详见上篇文章。但有时我们不得不使用当前环境的 Lock,一大原因是某些包在不同平台发布的包有着完成不同的依赖列表,这样会使得跨平台锁产生错误的结果。在 PDM 中,如果要禁用一个 Lock 策略,只需要:
pdm lock --strategy=no_cross_platform
这条命令会取消 cross_platform
策略,已使用的其他策略不会被影响。
static_urls
默认情况下,[[package]]
的 files
字段中只会记录包文件的文件名,而不包含 URL。这样做的好处是用户可以自由切换到其他 PyPI 的镜像源,PDM 安装时只会检查下载的文件名包含在 Lock 文件中。而如果启用了 static_urls
策略,PDM 会记录包文件的 URL,安装时就会直接通过这些 URL 下载安装包。这样也可以方便一些安全审计工具检查包的来源。
inherit_metadata
默认启用,PDM 会尝试为每个包计算它的最终 Marker(详见 上篇文章)。这样的好处是,安装时 PDM 只需要 pdm.lock
这一个数据来源,并且遍历 lock 文件和求值 Markers 这个过程使用 Python 标准库1就可以完成,不需要依赖 PDM 的其他组件。如果禁用这个策略,Marker 将不会记录在包中,这样 lock 文件中记录的信息就不足以让安装器决定是否安装这个包,安装过程会有些许变化。PDM 会通过要求安装和依赖列表,和 Lock 文件中的依赖信息,运行一次依赖解析过程,来取得最终需要安装的包版本的列表。
direct_minimal_versions
默认情况下,解析依赖时会从最新版本开始尝试,这样得到的 lock 结果通常包含了尽可能新的包版本。但有时为了测试库的兼容性,我们会希望看到它是否能在指定的最小依赖版本下工作。启用这个策略后,PDM 会尝试从最小版本开始解析依赖,得到的 lock 文件中的版本号就是最小版本的依赖。
--exclude-newer DATE
除了上述策略之外,PDM lock 还支持一个一次性选项 --exclude-newer
。这个选项的作用有点类似于时光机,当指定了一个时间或日期之后,PDM 解析依赖时会跳过那些晚于这个时间点上传的包版本。使用这个选项可以让 lock 文件是可复现的。需要注意的是,包的上传时间需要 PyPI 源的支持,它必须实现了 PEP 700,否则,这个包会被认为不满足条件并会被忽略。
更新策略
在你尝试更新 lock 文件中的包版本时,PDM 也提供了不同的更新策略,这些策略可以通过 --update-*
选项来指定,pdm add
,pdm lock
,pdm update
均支持这组选项。
--update-all
:更新所有包(直接依赖+间接依赖)到最新版本,也就是完全忽略 lock 文件中的版本信息--update-reuse
:只更新直接依赖的版本,复用 lock 文件中的间接依赖版本--update-eager
:更新指定依赖及其间接依赖到最新版本,复用 lock 文件中的其他依赖版本--update-reuse-installed
: 尽可能复用当前已安装的版本
更新依赖版本时,仍然会尊重 pyproject.toml
中指定的版本范围,这个行为可以通过 --unconstrained
选项关闭,即取消版本范围的限制。
到此为止,我们介绍了围绕 PDM 的 lock 文件的一系列功能和背后的逻辑,希望这些信息能帮助你更好地理解 PDM 的工作原理。