使用PDM来管理Python项目

概述

使用requirements.txt管理依赖的问题

正在开发一个使用第三方软件包(如fastapi)的Python项目。你需要指定这个依赖,以便其他开发人员和自动化系统能够运行你的应用程序。 于是,你决定在一个requirements.txt文件中包含flask的依赖项:

1
fastapi

这时候,一切在本地都正常运行,而在对应用程序进行了一番修改之后,你决定将其部署到生产环境。这就是事情变得有点棘手的地方…

上述的requirements.txt文件没有指定使用fastapi的哪个版本。在这种情况下,通过pip install -r requirements.txt将默认安装最新版本。这是可以的,除非最新版本中有接口或行为更改,导致我们的应用程序出现问题。

举个例子,假设fastapi的作者发布了一个新版本的fastapi。然而,它与你在开发过程中使用的版本不兼容。

现在,假设你将应用程序部署到生产环境并运行pip install -r requirements.txtPip获取了最新的不向后兼容的fastapi版本,就这样,你的应用程序在生产环境中崩溃了。

因此,为了解决问题,你尝试在requirements.txt中更加具体。你为fastapi依赖项添加了一个版本说明符。
这也被称为锁定依赖项

1
fastapi==0.99.0

fastapi依赖项固定到特定版本可以确保pip install -r requirements.txt设置了在开发过程中使用的确切fastapi版本。但它真的能做到吗?

因为fastapi本身也有依赖关系(pip会自动安装),但fastapi本身不为其依赖项指定确切版本。例如,它允许任何版本的starlette>=0.24.0

同样,为了这个例子,假设发布了一个新版本的starlette,但它为你的应用程序引入了一个致命错误。

当你在生产环境中执行pip install -r requirements.txt时,这一次你将得到fastapi==0.99.0,因为你已经锁定了这个依赖关系。然而,不幸的是,你将获得starlette的最新且有缺陷的版本。再次,产品在生产环境中崩溃。

因此,用包管理pdm解决上述问题。

pdm 是什么

pdm 差不多是 pip + venv,的结合体。可以类似 pip 用于管理第三方模块的管理,但是比 pip 的功能强大许多,同时还包含 venv 的虚拟环境管理功能。大致的功能如下:

  1. 管理第三方模块的安装与卸载
  2. 管理虚拟环境
  3. 管理虚拟环境的依赖
  4. 管理打包与发布 其中最重要的是 虚拟环境的依赖

名词解释:虚拟环境管理、模块管理、模块依赖管理

虚拟环境

虚拟环境是指内建的 venvvirtualenvconda 以及其他用来创建与管理 Python 虚拟环境的工具,不同的虚拟环境各自独立,存放的位置、安装的模块也都不一样。

模块管理、模块依赖管理

模块是指虚拟环境中安装的第三方模块及其版本。大多数项目对第三方库的版本都是有特定要求,如果对旧版本的项目使用新版本的依赖,可能会报很奇怪的错误。

当安装第三方模块时,第三方模块可能会安装自己依赖的模块。当安装两个以上模块时,就可能出现第三方模块的依赖出现冲突。这种情况一般是依赖的版本冲突。这种就叫做相关性依赖

PDM教程

安装

pdm 是一个命令行工具,安装之后就可以使用 pdm 指令。
可以将其安装全局环境或者是虚拟环境,我推荐安装在全局环境,这样在后面使用时不需要单独激活虚拟环境。

1
pip install pdm

安装之后就会在 python 解释器的安装目录下的 Scripts 目录里面出现 pdm.exe,因为在安装 python 解释器是配置过环境变量,然后就可以直接全局使用了。

新建项目

首先我们需要创建一个项目目录并且用pdm init初始化:

1
2
3
X:\> mkdir pdm-demo
X:\> cd pdm-demo
X:\ pdm-demo>pdm init

然后会跳出来一连串的互动对话,用于创建项目的配置文件,这里我就直接全部一路回车,
此时项目的目录接口如下:

1
2
pdm-demo
└── pyproject.toml

看一下生成的 pyproject.toml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[project]
name = "pdm-demo"
version = "0.1.0"
description = "Default template for PDM package"
authors = [
{name = "yulium", email = "yulium@163.com"},
]
dependencies = []
requires-python = "==3.11.*"
readme = "README.md"
license = {text = "MIT"}


[tool.pdm]
distribution = false

当您运行 pdm init 命令时,pdm 将要求在项目中使用 Python 解释器,这是安装依赖项和运行任务的基本解释器

PEP 582 相比,虚拟环境被认为更成熟,并且在 Python 生态系统和 IDE 中有更好的支持。因此,如果未配置 virtualenv 模式,则默认为 virtualenv 模式。

如果项目解释器(存储在 .pdm.toml 中的解释器,可使用 pdm info 查看)被使用,则将使用虚拟环境。

1
2
3
4
5
6
7
8
9
PDM version:
2.12.4
Python Interpreter:
C:\Users\<用户名>\AppData\Local\Programs\Python\Python311\python.EXE (3.11)
Project Root:
X:\pdm-demo
Local Packages:
X:\pdm-demo\__pypackages__\3.11
INFO: PDM 2.12.4 is installed, while 2.15.1 is available.

自动创建虚拟环境

默认情况下,PDM 倾向于像其他包管理器一样使用 virtualenv 布局。当你第一次在新的 pdm 管理的项目上运行 pdm install 时,它的 Python 解释器还没有确定,pdm 将在 X:/pdm-demo/.venv 目录下创建 virtualenvVenv,并在其中安装依赖项。在 pdm init 的交互会话中,pdm 还会要求为您创建 virtualenv

可以选择 PDM 用于创建 virtualenv 的后端。目前它支持三个后端:

  • virtualenv(默认)
  • venv
  • conda
    可以通过 pdm config venv.backend [virtualenv|venv|conda] 更改它。

创建自定义virtualenv

你可以用你想要的任何 Python 版本创建多个虚拟环境

1
2
# 创建基于3.11解释器的虚拟环境
X:\pdm-demo\ pdm venv create 3.11

virtualenv 的位置

PDM 将第一次尝试在项目中创建 virtualenv,除非.venv已经存在。其他virtualenv 将转到 venv.location 配置。它们被命名为 <project_name>-<path_hash>-<name_or_python_version> 避免名称冲突。使用 --name 选项创建的 virtualenv 将始终到此位置。可以通过 pdm config venv.in_project false 禁用在项目中创建 virtualenv

切换指定venv(必须存在)

可以命令 pdm use -f /path/to/venv

1
pdm use -f x:\venv\pdmtest\.venv

如果不确定项目下使用venv,可以命令pdm info

1
2
3
4
5
6
7
8
9
λ pdm info
PDM version:
2.12.4
Python Interpreter:
x:\venv\pdmtest\.venv\Scripts\python.exe (3.11)
Project Root:
x:/pdm-demo
Local Packages:

列出用这个项目创建的所有 virtualenv

1
2
3
$ pdm venv list
Virtualenvs created with this project:
- in-project: X:\pdm-demo\.venv

移除 virtualenv

1
2
3
4
$ pdm venv remove in-project
Virtualenvs created with this project:
Will remove: X:\pdm-demo\.venv, continue? [y/N]:y
Removed successfully!

激活 virtualenv

pdm-venv 不像 pipenvpoetry 那样生成子 shell,而是为您创建 shell,并将激活命令打印到控制台。这样就不会离开当前的外壳。然后你可以把输出给 eval 来激活 virtualenv

1
2
$ eval $(pdm venv activate in-project)
(test-project-for-test) $ # 进入虚拟环境