0%

Python setuptools 小结

上一篇文章对 Python 打包的整个过程进行了总结,这一篇文章将实际上手,使用 Python 的 setuptools 来生成自己的软件包,从而进一步熟悉 setuptools 的使用。

创建 wheel 包

这里我们将使用 setuptools 来创建一个 hello python wheel 包。安装 Python 的时候,setuptools 一般就已经自动安装了,如果没有安装,可以使用 pip install setuptools 安装 setuptools 本身。

首先来看我们软件包里的文件构成:

1
2
3
4
├── hello
│   ├── __init__.py
│   └── hello.py
└── setup.py
  • hello 目录:我们需要打包的源码目录
  • setup.py:使用 setup.py 来执行这个打包过程

源码目录 hello 里的 hello.py 文件内容如下:

1
2
def hello():
print("hello setuptools!")

接下来重点看 setup.py 的内容:

1
2
3
4
5
6
7
from setuptools import setup

setup(
name = 'hello',
version = '0.1',
packages = ['hello']
)
  • name 指定了软件包的名称
  • version 指定了版本
  • packages 指定了需要包含的软件包

接下来将其打包成 wheel 格式的二进制包:

1
$ python3 setup.py bdist_wheel

打包成功后,最终的目录结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── build
│   ├── bdist.macosx-10.15-x86_64
│   └── lib
│   └── hello
│   ├── __init__.py
│   └── hello.py
├── dist
│   └── hello-0.1-py3-none-any.whl
├── hello
│   ├── __init__.py
│   └── hello.py
├── hello.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
└── setup.py

7 directories, 10 files

我们真正生成的软件包位于 dist/hello-0.1-py3-none-any.whlbuild 目录保存了构建过程中产生的中间文件。

安装本地 wheel 包

在产生 wheel 包之后,我们可以将该 wheel 包上传到 PyPI 中,这里我们省去该过程,直接使用 pip 工具安装刚刚在本地产生的 wheel 包。我们进入一个干净的目录,然后将之前产生的 wheel 文件拷贝到该目录。然后使用如下命令安装本地的 wheel 包:

1
2
3
4
5
6
$ pip3 install hello-0.1-py3-none-any.whl
Processing ./hello-0.1-py3-none-any.whl
Installing collected packages: hello
Successfully installed hello-0.1
WARNING: You are using pip version 20.3.1; however, version 21.3.1 is available.
You should consider upgrading via the '/usr/local/opt/python@3.9/bin/python3.9 -m pip install --upgrade pip' command.

因为我的软件包指定的是 Python3,所以这里配套使用的是 pip3(电脑中同时安装了 Python2 和 Python3)。安装成功后,可以在 python 的 site-packages 看到该软件包的信息:

1
2
3
4
5
6
$ ls  /usr/local/lib/python3.9/site-packages/hello*
/usr/local/lib/python3.9/site-packages/hello:
__init__.py __pycache__ hello.py

/usr/local/lib/python3.9/site-packages/hello-0.1.dist-info:
INSTALLER METADATA RECORD REQUESTED WHEEL direct_url.json top_level.txt

直接使用 hello 包:

1
2
3
>>> from hello.hello import hello
>>> hello()
hello setuptools!

以上通过一个实际例子介绍了如何使用 setuptools 工具来生成 python wheel 包,接着再使用 pip 来安装生成的 wheel 包。

setuptools 更多用法

使用 setup.cfg

setuptools 支持使用配置文件来定义软件包的元数据以及各种定制选项,这样就不用在 setup() 函数中通过参数来指定。但是由于历史原因,不同的包分发管理工具(例如 distutils2、d2to1、PBR 等)都使用 setup.cfg 来保存打包配置,但是它们使用的 setup.cfg 配置语法与 setuptools 略有不同,具体的不同点可以参考这里

相比于把声明式的配置放入 setup.py 中,使用 setup.cfg 对包的使用者更加友好,用户可以通过编辑 setup.cfg 来修改配置。setup.cfg 的解析,是在读取 setup.py 之后,但在 python setup.py 命令执行之前。因此 setup.cfg 的配置会覆盖 setup() 函数里的参数配置,但无法覆盖 python setup.py 命令中提供的参数。

pbr

pbr (Python Build Reasonableness) 是一个Python 工具库,以统一的方式来管理 python setuptools。pbr 可以通过 setup hook 来读取 setup.cfg,并将解析后的结果作为 python setup.py 的参数。OpenStack 中的大量项目就是用 PBR 库来管理 setuptools 打包。

而且通过 pbr 库,可以减少许多配置的定义,因为 pbr 可以通过 Git 来获取版本、通过读取 README 来获得 Long Description、通过读取 requirements.txt 来获取 install_requires 等信息。而且通过 Git 可以自动生成 AUTHORSChangeLogMANIFEST.in 等文件。

为了使用 pbr 库,只需要按照如下方式编写 setup.py 即可:

1
2
3
4
5
6
7
8
#!/usr/bin/env python

from setuptools import setup

setup(
setup_requires=['pbr'],
pbr=True,
)

pbr 使用的 setup.cfg 格式与 setuptools 使用的 setup.cfg 格式略有不同,重要的 section 如下:

  • metadata:定义软件包的元数据
  • files:定义软件包需要安装哪些文件
  • entry_points:定义生成的 console scripts 和 Python 库的入口点。

这里再重点介绍一下 entry_points。通过 entry_points,软件包可以向其他发布模块通告自己的 Python 对象。一些可扩展的框架或应用可以通过名称找到 entry points,从而实现插件机制。entry_points 主要可以实现 2 个功能:

  • 动态发现服务和插件:通过在自己的软件包中定义 entry_points,其他应用框架就可以动态识别并加载这些新安装的软件包。可以认为这些软件包就是应用框架下的一些插件。
  • 生成可执行文件:通过 console_script entry_point 可以创建包装脚本,这些包装脚本可以直接运行,而程序的入口点就是 entry_point 中所指定的代码。这简化了 Python 代码的执行:不再需要通过 python $XXX.py 的方式启动程序,而是直接在终端上执行 $XXX。通过 gui_script 还可以生成 GUI 程序。

如果某个应用程序想要查找 entry_points(这些应用程序也就是我们所说的框架程序),Setuptools 工具推荐使用 importlib.metadata 模块。举个例子。例如我们自己编写的插件包,其 entry_points 定义如下:

1
2
my.plugins =
hello-world = timmins:hello_world

那么框架程序可以通过如下代码来动态加载插件:

1
2
3
4
5
from importlib import metadata
eps = metadata.entry_points()['my.plugins']
for ep in eps:
plugin = ep.load()
plugin()

这样框架程序并不需要提前知道插件的存在,只需要按照事先约定加载指定的 entry_points 组,就可以加载系统上相关的插件。

Reference

setuptools 官方文档
pbr使用文档
使用setuptools对Python进行打包分发
setuptools进阶