鸽了那么久终于决定把坑填完……本篇打算承接上一篇,讲讲python wheel包的创建。原则上需要涵盖的内容挺多的,但碍于篇幅只能把最相关的部分提炼出来。一般的开发者需要用到这部分内容的其实不多,但谁知道呢——也许哪一天愿意为开源社区提供自己写的程序包时,希望这篇博客能带来一定的指导。
python的打包工具有很多,setuptools
虽然已不能算是默认,但仍是目前最为流行的一种。首先,得确认自己的系统上有wheel
和setuptools
两个模块:
pip install wheel setuptools --user
setuptools
是用来完成打包动作的wheel
是setuptools
的一个衍生插件,提供一些命令行指令用于和wheel包交互py2.py3-none-any.whl
在名字中的包。对python2和python3、操作系统等均没有要求,兼容性极好。py3-none-any.whl
或py2-none-any.whl
在名字中的包。和universal wheel相比,仅仅在python的版本上有所要求。pip install
在执行的时候会去寻找和当前环境兼容的版本,如果未找到兼容的版本,但是找到源发行版的话,会下载源发行版在本地进行编译安装;如果源发行版也找不到,那安装就会报错了。
pyproject.toml
是在PEP518中被引入作为注明一个Python项目要用到什么编译工具的。在此之前人们没有办法让程序自动获知一个项目打包成wheel过程中需要的编译工具。除了PEP518之外,PEP517还指明了如何使用编译工具来完成打包编译,进一步实现了整体过程的标准化,统一了不同的编译工具和流程。当然,它们带来的好处还有一些,在这里暂不展开。
对于setuptools
用户来说,以下的内容,阐明一下setuptools的版本就够了:
[build-system]
requires = ["setuptools >= 40.6.0", "wheel"]
build-backend = "setuptools.build_meta"
笔者稍微浏览几个大的开源项目,都没有发现pyproject.toml
的存在。当然了,这只是统一化的过程,也许未来会做得更好一些。
setup.py
是打包过程的核心文件。我们以下面这个文件结构为例:
example_project/
├── exampleproject/
│ ├── __init__.py
│ └── example.py
└── README.md
对应地,一个最小setup.py
样例就如下:
from setuptools import setup, find_packages
setup(
name='example',
version='0.1.0',
packages=find_packages(include=['exampleproject', 'exampleproject.*']),
install_requires=[
'numpy',
'matplotlib',
'pandas'
]
)
name
:必要字段,指定了pip
搜索时的包名,不需要和项目文件夹同名。version
:必要字段,版本号,最好遵从国际标准的版本号命名方式。packages
:必要字段,指明所有的子模块位置。install_requires
:非必要字段,当你的包需要用到其他库的时候可以注明,方式跟平时写requirements.txt
一致。有些Python包在安装完成之后会自动提供命令行指令,比较著名的如jupyter-notebook
指令。这其实也是通过在setup.py
里添加字段来实现的。我们可以直接观察一下jupyter
项目的setup.py
:
...
entry_points = {
'console_scripts': [
'jupyter-notebook = notebook.notebookapp:main',
'jupyter-nbextension = notebook.nbextensions:main',
'jupyter-serverextension = notebook.serverextensions:main',
'jupyter-bundlerextension = notebook.bundler.bundlerextensions:main',
]
}
...
entry_points
同上面的name
等字段处于同等地位,其中添加了console_scripts
字样并将所有的命令行指令列出,并标明了每个指令对应的函数所在位置。从语法上来看并不难理解。
同一个包在不同的情况下会有不同的依赖关系,比如在开发时需要绘制图像观察各模块性能,但是部署到生产线上则不需要,这时就需要extras_require
关键字(仍旧以jupyter
项目为例):
extras_require = {
'test': ['pytest', 'coverage', 'requests',
'nbval', 'selenium', 'pytest', 'pytest-cov'],
'docs': ['sphinx', 'nbsphinx', 'sphinxcontrib_github_alt',
'sphinx_rtd_theme', 'myst-parser'],
'test:sys_platform != "win32"': ['requests-unixsocket'],
'json-logging': ['json-logging']
},
添加了这个字段的话,在执行普通的pip install jupyter
时,只会安装install_requires
字段里要求的依赖库;若执行pip install jupyter[test]
,则pytest
、coverage
等7个包会加装;执行pip install jupyter[docs]
,则sphinx
、nbsphinx
等5个包会加装。
下面的这些字段都属于元数据——当把包发布到PyPI时最好能加上这些字段,让其他用户知道包的维护者信息。
author
:作者名字author_email
:作者的邮箱description
:包的一句话描述url
:包所对应的项目的链接license
/license_files
:项目用到的证书设置完上述内容之后,打包只需要一句话即可:
python setup.py sdist bdist_wheel
或者,如果系统装有build
包(pip install build
)的话,也可以:
python -m build
完成之后,在项目的dist
目录文件下就可以找到源发行包(.tar.gz)和wheel包了(.whl)。
上述这些过程对universal类型的wheel是可用的,但是一旦遇上具有平台针对性的情况就不适用了,manylinux是linux环境下的一种解决手段。工业上比较常用的手段是通过持续集成(CI)服务器进行编译。如果想在本地执行manylinux编译的话,会需要Docker的支持。PyPA为开发者提供了一系列的Docker镜像和一个manylinux样例repo来支持这方面的工作。这其中的细节就不在本文中展开了,有兴趣的开发者可以点进这两个链接进行参考。
本篇介绍了使用setuptools
打包一个python包的简要步骤。这个过程中jupyter
项目让我学到了很多东西,它们的setup.py
写得非常漂亮,强烈建议有空去瞟一眼(见参考资料)
不过,前言中提到过,目前的打包手段已经不局限于setuptools
了,下一篇如果没有其他计划的话打算去学习一下poetry
看看它有什么不同。