Frost MingFrost's Blog

PDM 的内部实现 (3)

PDM
PDM 的内部实现 (3)

这篇文章将会介绍 PDM 的项目与环境管理,基于当前最新版本 2.15。英文版由 LLM 辅助翻译。

为项目指定 Python 版本

作为一个包管理与环境管理器,PDM 需要将你的项目依赖都安装在某个隔离的环境中,而在 Python 开发中多 Python 环境共存是家常便饭,所以 PDM 需要支持与其他 Python 版本对接。 通常,PDM 全局只需要安装一份,而且推荐安装在一个单独的虚拟环境中,也就是说这个环境中只包含 PDM 及其依赖,而你的项目依赖会安装在另一个干净的环境中。这里就会涉及到两个 Python 环境,每个环境对应一个 Python 解释器的路径。与之相对应的是 pip,如果你的机器上安装了多个 Python 版本,为了使用 pip,每个 Python 版本、虚拟环境中都需要一份 pip 的拷贝,然后所有的下载、安装都基于当前环境中的 Python 来进行。

那么 Python 解释器决定了哪些东西呢?主要有以下几个方面:

  • 依赖解析与安装时使用哪个 wheel?一个带 C 扩展的 wheel 文件名类似:numpy-1.26.0-cp310-cp310-macosx_10_9_x86_64.whl,其中会包含 Python 的版本和 ABI 等信息。
  • 安装依赖到哪个目录?例如在 Linux 上第三方库的默认安装路径是 <prefix>/lib/pythonX.Y/site-packages
  • 构建项目时使用哪个 Python 版本?它决定了构建生成的 wheel 文件名。

因此,在项目初始化时我们需要为项目指定 Python 的路径。这个解释器的路径会被保存在项目中的 .pdm-python 文件中。PDM 的几乎所有命令,如 lock/install/run/update/add等,都基于这个解释器进行。当 PDM 需要这些解释器本身的信息时,它会通过调用该解释器本身获取,绝不自作主张。例如,我们需要知道解释器的版本号,可以用:

python_version = subprocess.check_output([
    executable, '-c', 'import platform;print(platform.python_version())'
]).strip().decode()

其中 executable 为解释器的路径。运行 pdm info 可以获得项目环境的基础信息。

20240612165621

:::note 在使用 PDM 时,要杜绝使用 pip 的习惯,即在项目环境中安装 PDM 并用这个 PDM 管理同一个项目环境。 因为在运行 pdm remove 时,PDM 会清理掉项目没有指定的依赖,而它本身在也在这个环境中,所以会被清理掉。 :::

非绑定的 PEP 517 后端

PDM 的另一个重要且独特的设计,是它并不强制绑定 PEP 517 的后端。这样说可能略显抽象,在 PEP 517 中,完成构建这个动作涉及两个角色:真正负责构建打包的后端,以及准备环境调用构建工具的前端。PDM 和 pip 一样,只扮演了前端的角色,而后端则是由项目中的 pyproject.toml 文件中 [build-system] 指定的构建工具来提供。这样一来,PDM 只是提供了一些方便的修改 pyproject.toml 中元数据的操作界面,只要是支持 PEP 621 标准元数据的构建后端,都可以无缝的在 PDM 项目中使用,这些后端包括但不仅限于:

与 PDM 形成对比的是 Poetry,因为 Poetry 使用了一种独有的元数据格式,所以 Poetry 的项目只能用自家的 poetry-core 作为后端。

:::important[拥抱开放的生态] PDM 在设计之初,就从来没有想过要成为那个「一统江湖」的包管器。PDM 的哲学是,如果现有的工具已经足够好用,那么 PDM 就尽可能地与之集成,而不是取而代之。这也是为什么 PDM 尽可能地遵循标准,N 对 1 永远优于 N 对 M,可以起到事半功倍的效果。 :::

这个设计带来的结果是 PDM 永远不能假定项目使用了何种构建后端,于是当 PDM 需要一个当前项目的 wheel 时,会准备一个构建环境,安装依赖,并在 subprocess 中调用构建后端来得到 wheel。

评论