如何在 Python 使用 PDM 来管理项目

By iswbm / Published At 2024-06-01 / In categories Programming, Python

python

PDM 是一个新的 Python 的包管理器,也许你还未知晓它的存在,但实际上PDM 已经诞生两年,并在 2021 年发布 1.0 版本,目前最高的版本是 1.12.8。

在刚听到 PDM 时,我下意识认为它是 Python Development Manager,又一个和 Pipenv 和 Poetry 一样换汤不换药的虚拟环境管理工具。

一直到我翻到了作者的博客,才知道 PDM 的全称是 Python Development Master,比我想像的还要牛逼一个档次。

值得一提的是,PDM 的作者是 PyPa 成员、Pipenv 目前主要的维护者之一,最重要的是,他是中国人,因此这是一款国人开发的工具。

1. Why PDM?

早期的包管理器(如 Pipevn,Poetry),都是基于虚拟环境的,虚拟环境主要是为了隔离项目开发环境,但如果涉及到虚拟 环境嵌套虚拟环境,问题就难搞了,经常会出现问题。

PDM 得益于一个 2018 年的 PEP 提案(PEP582,Python local packages directory),完全摒弃了虚拟环境。

作者的博客上来看,当初之所以要重复造个轮子,完全是因为 Pipenv 和 Poetry 都不够好用,正好有 PEP582 ,可以开发一个划时代的 Python 包管理工具,它就是 PDM 。

PDM 包含如下特性:

  • PEP 582 本地项目库目录,支持安装与运行命令,完全不需要虚拟环境
  • 一个简单且相对快速的依赖解析器,特别是对于大的二进制包发布。
  • 兼容 PEP 517 的构建后端,用于构建发布包(源码格式与 wheel 格式)
  • 拥有灵活且强大的插件系统(有插件系统直接就拉开一个档次)
  • PEP 621 元数据格式
  • pnpm 一样的中心化安装缓存,节省磁盘空间

尽管 PDM 是国人开发,但考虑到国际化,官网文档是全英文的。

我花了整整一天,通读完文档,消化了 70% 的 PDM 用法,现将心得整理分享出来,会对你上手 PDM 有帮助。

关于 PDM,内容挺多的,打算分两篇文章来完整地介绍它:

  • 面向新手的入门级教程
  • 面向骨灰级选手的教程

本篇是第一篇,先让大家对 pdm 的基本用法有一个框架性的理解,而 pdm 真正竞争力请持续关注后续文章。

2. 安装 PDM

PDM 的安装方法有很多种,在官网上就有 6 种,比如 pip、pipx、homebrew 等

在以前的文章中,我推荐过 pipx 工具,在安装那种命令行应用的包时非常好用。

而此时 PDM 就是一个命令行工具,因此我也推荐使用 pipx 安装,方便统一对命令行进行管理

执行 pipx install pdm 即可安装

PDM 只有 Python 3.7+ 的版本才能使用,使用其他的方法安装,要先保证你的 Python 版本,但使用 pipx 则不需要你去操心。

3. 初始化 PDM

执行 pdm init 就会开始初始化,初始化的时候,会让你选择项目的一些信息

  • 是否要上传 PyPI
  • 依赖的 Python 版本
  • License 类型
  • 作者信息
  • 邮箱信息

我机器上有 Python 2.7 和 Python 3.10 两个版本,在初始化项目时会把机器上的所有 Python 版本都扫描出来了,会让选择项目的 Python 版本。

完成之后,PDM 会将你的选择以 toml 格式写入 pyproject.toml 配置文件中。

4. PDM 用法

pdm 有非常多的命令,使用 -h 可以看到帮助菜单

4.1 安装包

和 Poetry 一样,安装使用的是 add 命令,但 pdm 的 add 比 poetry 好用,主要体现在分组,具体请关注后续文章

4.2 查看包

使用 pdm list 可以以列表形式列出当前环境已安装的包

再加个 --graph 就能以树状形式查看,直接依赖包和间接依赖包关系的层级一目了然

pdm list 还有两个选项:

  • --freeze:以 requirements.txt 的格式列出已安装的包
  • --json:以 json 的格式列出已安装的包,但必须与 --graph 同时使用

要查看某个包的某体详情,直接用 pdm show 即可

4.3 删除包

删除包使用的是 remove 命令

4.4 项目配置

不加任何参数,可以打印出该项目的环境配置

想要修改的话,只要加 key 和 value 做为参数即可,以修改 pypi 镜像代理为例

原来上面是豆瓣源,现在我要改成阿里源,只需要执行如下命令,可比 poetry 方便多啦~

pdm config 里面有非常多的配置,想要一一搞清楚的可以去官网查阅:https://pdm.fming.dev/configuration/

4.5 运行命令

想要在 pdm 的环境中执行命令或者项目,可以使用 run 命令,若是执行项目时,有诸多参数,可以在 pyproject.toml 配置命令别名,具体用法,请往后看

4.6 查看环境

使用 info 命令,可以查看当前项目的环境信息

4.7 更新包

更新的话,简单的场景下,使用下面这两条即可

# 更新所有包
pdm update 

# 更新某个包
pdm update <pkg>

复杂的场景,pdm 也都为你考虑到了,它提供了很多选项,可以根据需要使用(以下如有解释错误,请帮忙指正)

  • --save-compatible:项目依赖可兼容的版本
  • --save-wildcard:保存通配符版本(暂不明白)
  • --save-exact:保存有指定确切版本的包
  • --save-minimum:保持最小版本的包
  • --update-reuse:尽量只更新命令行中指定的包,其依赖包能不更新则不更新
  • --update-eager:更新某个包顺带更新其依赖包(递归升级)
  • --prerelease:允许提前释放(暂不明白)
  • --unconstrained:忽略包版本的约束,可将包升级至最新版本
  • --top:仅更新有在 pyproject.toml 的包
  • --dry-run:试运行,而不去修改 lock 文件
  • --no-sync:只更新 lock 文件,但不更新包

如果你的依赖包有设置分组,还可以指定分组进行更新

pdm update -G security -G http

也可以指定分组更新分组里的某个包

pdm update -G security cryptography

再加个 -d 就可以再指定 dev 依赖

# 更新所有的 dev 依赖
pdm update -d

# 更新 dev 依赖下某个分组的某个包
pdm update -dG test pytest

同样地,也可以指定 --prod 或者 --production 升级非 dev (即生产)的包。

4.8 切换 py

当你在初始化 pdm 项目时,就已经选定了当前的 Python 版本和可用的 Python 版本范围,后面如果想更改,可以使用 use 命令,但版本要受之前设定的版本范围约束。

假设允许范围是 python 3.9+,当前使用的是 python 3.10,可以直接切换过去。

pdm use python3.9

5. 命令别名

在 pyproject.toml 添加 [tool.pdm.scripts] 可以设置快捷命令别名,若项目的执行有非常多的参数,这种设定别名的方法将很有用。

[tool.pdm.scripts] 有两种形式

# 第一种
[tool.pdm.scripts]
start = "python main.py"

# 第一种
[tool.pdm.scripts]
start = {cmd = "python main.py"}

但若想在参数中加注释,就必须得使用第二种方法,例如这样

[tool.pdm.scripts]
start = {cmd = [
    "flask",
    "run",
    # Important comment here about always using port 54321
    "-p", "54321"
]}

除了 cmd 之外,还有两个参数

一个是 shell 参数,从输出来看你应该和看出和 cmd 的区别,和 subprocess.Popen() with shell=True 差不多一个意思

一个是 env_file 参数,可以指定配置环境变量的文件

[tool.pdm.scripts]
start.cmd = "flask run -p 54321"
start.env_file = ".env"

如果想要把这个环境变量的文件不仅限于某个命令,而是 pdm run 全局,可以这样配置

[tool.pdm.scripts]
_.env_file = ".env"

--list 或者 -l可以查看所有设置的快捷别名

对于每一个快捷命令,都可以设置 pre 和 post 命令:

  • pre 命令:在每次快捷命令执行前会执行
  • post 命令:在每次快捷命令执行后会执行
[tool.pdm.scripts]
pre_compress = "{{ Run BEFORE the `compress` script }}"
compress = "tar czvf compressed.tar.gz data/"
post_compress = "{{ Run AFTER the `compress` script }}"

6. 自动补全

pdm 的命令虽多,但并不复杂,并不太需要使用自动补全,若你真的需要补全,也可以实现。

对于不同的 shell,自动补全的配置方式都不太一样,这个在官网上有详细的说明。

如果你和我一样使用的 zsh,可以参照我的配置方式。

截图中间有一步是 vim ~/.zshrc ,是将 pdm 插件配置到 zsh 中

plugins=(git z macos extract zsh-syntax-highlighting zsh-autosuggestions pdm)

7. 方案兼容

其他方案迁移到 pdm

pdm 足够好用,也足够开放,如果你当前使用的是其他的包管理器,比如 pipenv ,poetry,或者还在用最原始的 requirements.txt ,你也可以很方便的迁移到 pdm 中来:

  • 使用 pdm import -f 直接转换到 pyproject.toml,然后使用 pdm lock 写入 pdm.lock 中,最后使用 pdm sync 安装包
  • 执行 pdm init 或者 pdm install 的时候,会自动识别你当前的依赖情况并转换

pdm 迁移到其他方案

同样的,你也可以当 pdm 管理的项目,导出为其他方案

pyproject.toml 和 pdm.lock是 pdm 的两个核心文件。

pdm 做为一个后起之秀,也没有忘本,它支持:

  • 将 pyproject.toml 转成 setup.py

    pdm export -f setuppy -o setup.py
    
  • 将 pdm.lock 转成 requirements.txt

    pdm export -o requirements.txt
    

8. PEP 582 原理

在 pdm 的环境之下,它是优先从项目里的 __pypackages__ 搜索包的(安装时也是如此)

这是如何实现?其实原理非常简单。

使用 -m site,可以查看当前 Python 环境包搜索目录的顺序

从下图可以看到使用 pdm 工具后,无疑就是在全局的 site-packages 目录之前加上项目目录里的 __pypackages__ 路径,使得 __pypackages__ 的搜索优先级高于全局的 Python 环境。

是的,pdm run 命令默默帮你做了两件事:

  1. 在执行命令前,插入 __pypackages__ 目录到 PYTHONPATH 中
  2. 在执行命令后,删除 PYTHONPATH 中的 __pypackages__ 目录

如果你不想每次执行命令都加个 pdm run ,可以直接将该变量设置到当前的环境中,只需要你使用 pdm --pep582 里的 shell 程序来启用 pep582

9. 全局项目与局部配置

所有的 pdm 命令都可以使用 -p-g 选项来指定你要操作的是全局项目还是局部项目,若你没有指定 -p-g ,并且刚好处在局部项目目录之下,pdm 甚至会自动识别该项目,而优先操作局部项目。

而 pdm config 里的配置,优先逻辑却与上面相反。

当你不使用 -l/--local 时,pdm config 则优先操作全局的 pdm config ,因此如下三条命令是等价的

pdm config pypi.url http://pypi.douban.com/simple
pdm config -g pypi.url http://pypi.douban.com/simple
pdm config --global pypi.url http://pypi.douban.com/simple

只有当你明确指定 -l/--local 才会操作局部项目的 pdm config

pdm config -l pypi.url http://pypi.douban.com/simple
pdm config --local pypi.url http://pypi.douban.com/simple

全局配置与局部配置对应配置文件位置是:

  • 全局配置:~/.pdm/config.toml
  • 局部配置:<PROJECT_ROOT>/.pdm.toml

局部项目的配置文件名由 . 开头,因此是隐藏文件,需要你使用 ls -a 才可以看到。

需要千万注意的是,若你想修改其他项目的局部配置,不仅需要加 -p/--project 还要加 -l/--local 选项

pdm config -l -p <project_path> pypi.url http://pypi.douban.com/simple
pdm config --local --project <project_path> pypi.url http://pypi.douban.com/simple

pdm 会优先取局部配置,若局部配置里没有,才会去取全局的配置。

如果你配置完某个参数后,想删除的话,可以指定 -d/--delete 参数,但全局配置与局部配置的删除逻辑又不一样。

不加 –local 的话,说明操作全局配置,pdm 并不直接将配置删除,而是还原该全局配置为默认值,以 auto_global 为例,它的默认值为 False,现在我先将其改为 True,再将其删除,发现 auto_global 只是被重置了而已。

而加了 –local 的话,说明操作局部配置,若局部配置里刚好有该配置,则将其删除,若没有该配置,则直接返回成功。

还有一点需要特别注意,pdm 中并非所有的配置都有全局和局部之分,就比如 use_global ,你加个 --local 会直接报 ValueError ,不允许将其设置为局部配置,因为没有意义~

10. 全局项目与局部项目

10.1 pdm 的两种项目

正常情况下,我们都是使用 pdm init 来初始化项目时,而初始化后,会在本地生成一个 pyproject.toml 文件和 __pypackages__ 目录,这只是 pdm 默认的模式。

为理解方便,pdm 管理的项目,我将其分为两种

  • 全局项目
  • 局部项目

实际上,咱在初始化的时候,还可以再加个参数 -g/--global ,它表示该项目将使用全局的 pdm 的 pyproject.toml 配置文件。

  • 若你是首次使用 -g/–global 参数,那么它将创建 ~/.pdm/global-project/ 目录,将在该目录下创建 pyproject.toml
  • 若你是二次使用 -g/–global 参数,那么你输入的信息将覆盖原来的 pyproject.toml 的配置

使用过 -g 后,你再使用 pdm list -g 查看全局已安装的包时,就会发现多了个 global-project 的包?然后在 Location 列有 -e /Users/iswbm/.pdm/global-project 指定全局模式 pyproject.toml 及 pdm.lock 所在的位置

默认情况下,当你没有指定 -g 参数去执行 pdm 命令时,它只会先从本地目录读取 pyproject.toml

而当你指定 -g 命令,不管你本地项目是否为一个 pdm 创建的项目,是否存在 pyproject.toml,pdm 都只会去读取全局的 pyproject.toml

如下所示,我分别在全局和本地的 pyproject.toml 添加不同的 shell 命令,在加 -g 和 不加 -g 的效果就非常明显了

上面之所以说默认情况下如果本地目录没有 pyproject.toml ,pdm 并不会去使用全局的 pyproject.toml。

是因为 pdm 提供了一个开关(auto_global),可以自行改变这种默认的行。

10.2 全局项目的包装在哪

在局部项目下,安装的包是装在了 __packages__ 下,那使用 -g 指定全局环境的包,是装在哪里呢?

尝试在 ~/.pdm 目录下寻找,并没有任何发现

实际上,它是安装在 pdm 工具本身所在虚拟环境中,而不是你机器全局的 Python 环境中。

10.3 局部项目的检测?

使用 -g 参数后,你可以在机器的任何位置执行命令,而不加 -g 呢?难道要当前目录下有 pyproject.toml 才可以?

这样岂不是要求你需要处于项目的根目录才可以 pdm 管理局部项目?

经过测试,可以发现 pdm 并不需要你处于项目的根目录,只要你所处的位置的父级目录中有发现 pyproject.toml 一样可以管理局部项目(实际是不是这个逻辑,没有看源代码还不确定)

10.4 如何指定项目?

上面介绍了两种 pdm 的项目:

  • 全局项目:可以在任意位置使用 pdm 去管理全局项目
  • 局部项目:只能在项目根目录或及子目录里使用 pdm 管理项目

而实际使用中,你可能还希望能在任意位置去管理非全局项目,这个需求只要你加 -p/--project 即可实现

那有没有办法使用 a 项目的 pyproject.toml 及 pdm.lock 去管理 b 项目呢?

答案是不行。

pyproject.toml 及 pdm.lock 所在的位置就是 pdm 管理项目的根目录,无法通过 -p 来实现该需求。

所有的 pdm 命令都可以使用 -p-g 选项来指定你要操作的是全局项目还是局部项目,若你没有指定 -p-g ,并且刚好处在局部项目目录之下,pdm 甚至会自动识别该项目,而优先操作局部项目。

而 pdm config 里的配置,优先逻辑却与上面相反。

当你不使用 -l/--local 时,pdm config 则优先操作全局的 pdm config ,因此如下三条命令是等价的

pdm config pypi.url http://pypi.douban.com/simple
pdm config -g pypi.url http://pypi.douban.com/simple
pdm config --global pypi.url http://pypi.douban.com/simple

只有当你明确指定 -l/--local 才会操作局部项目的 pdm config

pdm config -l pypi.url http://pypi.douban.com/simple
pdm config --local pypi.url http://pypi.douban.com/simple

全局配置与局部配置对应配置文件位置是:

  • 全局配置:~/.pdm/config.toml
  • 局部配置:<PROJECT_ROOT>/.pdm.toml

局部项目的配置文件名由 . 开头,因此是隐藏文件,需要你使用 ls -a 才可以看到。

需要千万注意的是,若你想修改其他项目的局部配置,不仅需要加 -p/--project 还要加 -l/--local 选项

pdm config -l -p <project_path> pypi.url http://pypi.douban.com/simple
pdm config --local --project <project_path> pypi.url http://pypi.douban.com/simple

pdm 会优先取局部配置,若局部配置里没有,才会去取全局的配置。

如果你配置完某个参数后,想删除的话,可以指定 -d/--delete 参数,但全局配置与局部配置的删除逻辑又不一样。

不加 –local 的话,说明操作全局配置,pdm 并不直接将配置删除,而是还原该全局配置为默认值,以 auto_global 为例,它的默认值为 False,现在我先将其改为 True,再将其删除,发现 auto_global 只是被重置了而已。

而加了 –local 的话,说明操作局部配置,若局部配置里刚好有该配置,则将其删除,若没有该配置,则直接返回成功。

还有一点需要特别注意,pdm 中并非所有的配置都有全局和局部之分,就比如 use_global ,你加个 --local 会直接报 ValueError ,不允许将其设置为局部配置,因为没有意义~

11. PDM 的原理

在 pdm 的环境之下,它是优先从项目里的 __pypackages__ 搜索包的(安装时也是如此)

这是如何实现?其实原理非常简单。

使用 -m site,可以查看当前 Python 环境包搜索目录的顺序

从下图可以看到使用 pdm 工具后,无疑就是在全局的 site-packages 目录之前加上项目目录里的 __pypackages__ 路径,使得 __pypackages__ 的搜索优先级高于全局的 Python 环境。

是的,pdm run 命令默默帮你做了两件事:

  1. 在执行命令前,插入 __pypackages__ 目录到 PYTHONPATH 中
  2. 在执行命令后,删除 PYTHONPATH 中的 __pypackages__ 目录

如果你不想每次执行命令都加个 pdm run ,可以直接将该变量设置到当前的环境中,只需要你使用 pdm --pep582 里的 shell 程序来启用 pep582

12. PDM 缓存机制

pdm 引入了 pep 582 的本地包目录,有很多人在质疑:每个项目都在自己项目目录之下,那和 venv 虚拟环境有什么区别?

不少人对于虚拟环境及 pep 582 的理解不深,有这个疑问也是正常的。

首先,第一点不同,虚拟环境有自己的 Python 解释器,而 pep 582 并没有新增 Python 解释器,因此 pep 582 更加轻量。

然后,第二点不同,就是我们今天的核心内容,pdm 缓存机制的支持。

如果多个 pdm 项目,依赖相同版本的同一个 python 包,正常情况下,每个项目会自己存一份到自己的 __pypackages__ 目录下。

但这样有几个问题:

  1. 浪费磁盘空间
  2. 安装速度慢

你或许会认为,现在磁盘是最便宜的硬件了,浪费一点无所谓,但有些 Python 项目的依赖包多到你无法想象,比如世界上最大的 Python 项目 OpenStack ,依赖包更是达到了上千个,就算你不心疼你的磁盘,那你的时间肯定很宝贵吧?

你创建一个新的 pdm 项目,要重头安装一遍这么多依赖包,没个一天时间也搞不定,到时你就知道缓存的重要性了。

12.1 开启缓存

pdm 默认是关闭 cache 的,如有需要,可以通过如下命令进行开启

$ pdm config install.cache on

与缓存相关的配置有三个

  • install.cache:是否开启缓存
  • install.cache_method:选择连接缓存的方式
  • cache_dir:指定缓存的存放目录

关于 cache_dir 如无特殊需要,可以不用管,用默认的目录即可

/Users/iswbm/Library/Caches/pdm

比较难以理解的,值得一讲的是 install.cache_method,它的值有两种:

  • symlink:以软链接的方式连接
  • pth:以 pth 的方式连接

关于它们的区别,我在后边有详细的讲解,请继续往下

12.2 简单示例

这边以一个简单的示例,让你了解缓存的工作原理。

首先我创建两个 pdm 项目

# 初始化第一个 pdm 项目
mkdir pdm-demo1 && cd pdm-demo1
pdm init


# 初始化第二个 pdm 项目
mkdir pdm-demo2 && cd pdm-demo2
pdm init

在 pdm-demo1 下,安装 typer 的包

pdm add typer

然后进入 python 交互式解释器,试着导入一下,查看导入的 typer 包路径是什么?

可以发现,存放的目录正是 cache_dir 所配置的目录

然后进入 pdm-demo2 下,同样安装 typer 包

pdm add typer

同样进入 python 交互式解释器,试着导入一下,查看导入的 typer 包路径是什么?

可以发现,导入的 typer 与之前 pdm-demo1 的路径一致,说明这两个项目用的同一个 typer 包,避免了同个包同个版本的重复安装。

12.3 缓存的原理

关于缓存原理,其实并不难,对于不同的 install.cache_method 原理也不一样

cache_method=symlink

symlink 是默认的连接方式,也是最好理解的一种方式。

当你安装了 typer 包后,在本地包目录下就可以看到 typer 通过一个软链接的方式指向了缓存目录下的 typer 包

cache_method=pth

对于 .pth 相信有不少人不清楚它的用法和原理,这里简单提一下。

当 Python 在遍历已知的库文件目录过程中,如果发现有 .pth 文件,就会将文件中所记录的路径加入到 sys.path 设置中,于是 .pth 文件说指明的库也就可以被 Python 运行环境找到了。

焦点回到 pdm 中来,如果你使用 cache_method=pth 的模式,你每安装一个包,在你的本地包目录下就会生成一个 .pth 文件,里面记录要缓存的包的 lib 目录。

这样一来,当 Python 在 __pypackages__ 目录下查找包时,一旦发现有 .pth 文件,就会把 .pth 文件中记录的路径加入 sys.path 中去。

在上面的例子中,查看 __pypackages__ 目录,可以发现有许多 aaa_xxx.pth 的文件,而这些文件的内容,即是我们缓存目录下对应包的 lib 目录

12.4 缓存的管理

pdm 管理缓存的命令帮助如下

  • pdm cache clear:清理所有的缓存
  • pdm cache info:查看所有的缓存信息
  • pdm remove [pattern]:移除匹配到的文件
  • pdm cache list:列出所有在缓存中的 wheel 文件

13. PDM 在 PyCharm 的使用

昨天有读者在评论区问,如何设置 PyCharm 让其支持 pdm 的环境?

做为一个骨灰级 PyCharm 用户,设置方法其实很简单,这里简单说一下

首先,使用 PyCharm 打开你的 pdm 项目,并在该 pdm 中安装 click 包

然后右键将 __pypackages__/3.10/lib 目录标记为 Sources Root,有了这个标记,PyCharm 会优先从该目录中去导包,并且该目录的优先级是比全局的环境还高的

找到你的 pdm 的 Python 解释器,将其设置中 PyCharm 中,如果你是安装我的教程来安装的,就可以去 pipx/venv 目录下寻找

到这里,一切设置就 OK 了

那如何检验效果呢?连代码都不用运行,我们只要在 main.py 中写导入 之前安装的 click 包

import click

然后按住 ⌘ (windows 电脑是 ctrl ) + 鼠标左键点击 click,看看会跳转到哪里?

若是跳转到 __pypackages__/3.10/lib/click 目录下,则说明 pdm 的环境已经生效,若跳转到其他地方,则说明设置失败。

具体的验证过程我录了个 GIF 动图,供大家参考

参考:https://pdm.fming.dev/#pycharm

iswbm

Author

iswbm

Cloud Computing & Container & Front-end & Back-end R&D Engineer. I like to explore new technologies, and in my spare time, I also play around with Logseq and other efficiency tools. You can follow me on GitHub to learn more, or add me on WeChat (stromwbm) to communicate with me.