英文 | The state of Python Packaging【1】
原作 | BERNAT GABOR
译者 | 豌豆花下猫
声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。
pip
19.0 已经于 2019 年 1 月 22 日发布。在其功能列表中,最值得注意的是它现在支持 PEP-517,默认情况下是支持的,如果项目的根目录中有一个 pyproject.toml。该 PEP 于 2015 年创建,并于 2017 年被接受。尽管 pip 花了一段时间才实现它,但该版本及其后续问题却表明,很多人根本不熟悉它。
如果你想了解 Python 打包(packaging)生态的现状及将来如何演变,请继续阅读。我们希望,即使上述提到的 Python 增强提案(译注:即 PEP,关于 PEP 的介绍,请阅读这篇文章),如今可能会引起一些不愉快,但从长远来看,我们将从中受益。
我大约在三年前加入了 Python 开源社区(尽管使用它已有 8 年之久)。从早期开始,我就听说 Python 打包有一点黑匣子的名声。它有很多未知的内容,人们通常只复制其它项目的构建配置文件,就使用上了。
在尝试更好地理解这个黑匣子,并对其进行改进的过程中,我已经成为了 virtualenv 和 tox 项目的维护者,偶尔也为 setuptools 和 pip 做些贡献。
我希望对这个主题进行详尽的(并希望是一个较高水平的)论述,并决定将其分为三个部分。在这第一篇文章中,我将对 Python 打包的工作方式及其所具有的打包类型进行大概介绍。在第二篇文章中,我将详细地介绍软件包的安装方式,以及 PEP-517/518 是如何尝试对其进行改进的。最后,我再专门写另一篇文章,以介绍在引入这些改进时,我们吸取的一些痛苦的教训。
事先声明,我将主要关注 Python 官方的打包系统(即 pip、setuptools,因此没有 conda 或特定于操作系统的打包程序)。
Marcus Cramer 摄/Unsplash--人们第一次凝视 Python 打包时的脸
一个示例项目
为了讲这个故事,我需要先讲讲如何分发 Python 软件包的故事;更具体地说,包的安装在过去是如何运作的,以及我们希望它在将来如何运作。
为了有一个具体的示例,让我介绍一下我的很棒的示例库:pugs
。这个库相当简单:它只生成一个名为 pugs 的包,仅包含一个名为 logic 的模块。关于 pugs,你猜对了,logic 被用于生成随机的引号。这是一个展现为源码树(source tree)的简单示例结构(可以在gaborbernat / pugs 【2】里获得):
pugs-project
├── README.rst
├── setup.cfg
├── setup.py
├── LICENSE.txt
├── src
│ └── pugs
│ ├── __init__.py
│ └── logic.py
├── tests
│ ├── test_init.py
│ └── test_logic.py
├── tox.ini
└── azure-pipelines.yml
这里有四类独特的内容:
我们的pugs
包在用户机器的解释器上能用,意味着什么?在理想情况下,一旦启动解释器,用户应该能够 import 它,并调用其中的函数:
-
业务逻辑代码(src 文件夹中的内容)
-
测试代码(tests 文件夹和 tox.ini)
-
包代码和元数据(setup.py、setup.cfg、LICENSE.txt、README.rst--请注意,我们如今使用的是事实上的标准打包工具setuptools【3】)
-
有助于项目管理和维护的文件:
-
- 持续集成(azure-pipelines.yml)
- 版本控制(.git)
- 项目管理(例如潜在的 .github 文件夹)
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pugs
>>> pugs.do_tell()
"An enlightened pug knows how to make the best of whatever he has to work with - A Pug's Guide to Dating - Gemma Correll"
Ryan Antooa 摄/Unsplash--让我们开始吧,兴奋!
Python 包的可用性
Python 怎么知道什么可用或不可用?简短的答案是,它不知道。至少不在前期知道。相反,它将尝试加载,并动态地检查是否可用。
它从哪里加载?有许多可能的位置,但是在大多数情况下,我们说的是从文件系统的文件夹中加载。这个文件夹在哪里呢?对于给定的模块,可以打印该模块的表示(representation)来找出:
>>> import pugs
>>> pugs
<module 'pugs' from '/Users/bernat/Library/Python/3.7/lib/python/site-packages/pugs/__init__.py'>
你会发现文件夹的位置取决于:
- 软件包的类型(三方库或者标准库的内置/aka部分)
- 它是全局的或仅限于当前的用户(请参阅PEP-370【4】)
- 以及它是系统 Python 还是一个虚拟环境
但是一般来说,对于给定的 Python 解释器,可以通过打印出 sys.path 变量的内容,来找到可能的目录列表,例如在我的 MacOS 上:
>>> import sys
>>> print('
'.join(sys.path))
/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/Users/bernat/Library/Python/3.7/lib/python/site-packages
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages
对于第三方软件包,会是一些 site-packages 文件夹。在以上示例中,请注意哪些是在整个系统范围内,哪些仅属于一个特定的用户。这些包是如何被放在此文件夹中的?它一定是由某些安装程序放在那里的。
下图展示了大多数的运行情况:
- 开发者在文件夹(称为源码树)内编写一些 Python 代码。
- 然后,某些工具(例如 setuptools)将源码树打包以进行重新分发。
- 生成的软件包通过另一个工具(twine),上传到可以被终端用户计算机访问的中央存储仓(通常为https://pypi.org【5】)。
- 终端用户计算机使用一些安装程序来查找、下载和安装相关软件包。安装操作最终是在 site-packages 文件夹内,创建正确的目录结构和元数据。
Pinho/摄--在探索新鲜事物
Python 包的类型
在安装时,软件包必须生成至少两种类型的内容,以放入 site-packages 中:有关软件包内容的元数据文件夹,其中包含 {package}-{version} .dist-info 和业务逻辑文件。
/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-37.pyc
│ └── logic.cpython-37.pyc
└── logic.py
/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs-0.0.1.dist-info
├── INSTALLER
├── LICENSE.txt
├── METADATA
├── RECORD
├── WHEEL
├── top_level.txt
└── zip-safe
发行信息(dist-info)文件夹描述了该软件包:用于安装该软件包的安装程序、该软件包所附的许可证、在安装过程中创建的文件、顶层 Python 软件包是什么、该软件包暴露的入口等等。在PEP-427【6】 中可以找到每个文件的详细说明。
我们如何从源码树中获得这两种类型的内容呢?我们面前有两条截然不同的路径:
- 从我们的源码树生成此目录结构和元数据,将其压缩为单个文件,然后将其发布到中央软件包存储仓。在这种情况下,安装程序必须下载软件包并将其解压到 site-packages 文件夹中。我们将这种类型的包称为
wheel
包。 - 或者,你可以创建一个包含软件包源码的归档文件,构建所需的脚本和元数据,以生成可安装的(installable)目录结构,然后将其上传到中央存储仓。这称为源码分发或 sdist。在这种情况下,安装程序还有很多工作要做,它需要解压归档文件,运行构建器,然后再将其复制。
这两个方法的区别主要在于包的编译/构建操作发生在哪里:在开发者的计算机上还是在终端用户的计算机上。如果它发生在开发者的一边(例如在 wheel 的情况下),则安装过程非常轻巧。一切都已经在开发机器上完成了。用户机器的操作仅是简单的下载和解压。
在本例中,我们使用 setuptools 作为构建器(从源码树生成要放入 site-packages 文件夹中的内容)。因此,为了在用户机器上执行构建操作,我们需要确保在用户机器上有合适版本的 setuptools (如果你使用的是 40.6.0 版的功能,则必须确保用户具有该版本或大于该版本)。
要考虑的另一种情况是 Python 提供了从其内部访问 C/C++ 库的能力(在需要的地方获得额外的性能)。这样的软件包被称为 C 扩展包(C-extension packages),因为它们利用了 CPython 提供的 C 扩展 API。
此类扩展需要编译 C/C++ 功能,才能适用与其交互的 C/C++ 库和当前 Python 解释器的 C-API 库。在这些情况下,构建操作实际上涉及到调用一个二进制编译器,而不仅仅是像纯 Python 包(例如我们的 pugs 库)那样,生成元数据和文件夹结构。
如果在用户计算机上进行构建,则需要确保在构建时,有可用的正确的库和编译器。现在这是一项相对困难的工作,因为有些特定于平台的二进制文件,也是通过平台打包工具分发的。这些库的缺失或版本不匹配通常会在构建时触发隐秘的错误,使用户感到沮丧和困惑。
因此,如果可能的话,始终选择将 package 打包成 wheel。这将完全避免用户缺少正确的构建依赖项的问题(纯 Python 类型如 setuptools 或二进制类型的 C/C++ 编译器)。即使这些构建依赖项易于配置(例如,使用纯 Python 构建器--例如 setuptools),你完全可以避免此步骤,来节省安装的时间。
话虽如此,仍然有两种需要提供源码分发的情况(即使在你提供 wheel 的情况下):
- C 扩展的源码分发往往更易于审核,因为人们可以阅读源代码,从而在其内容上有更高的透明度:许多大型公司的环境出于此单一原因,更倾向于使用 wheel(它们通常会将此扩展到纯 Python wheel,主要是为了避免对哪些是纯 Python 和什么不是做分类)。
- 你可能无法为每个可能的平台都提供一个 wheel(在使用 C 扩展包的情况下,尤其如此),在这种情况下,源码分发可以让这些平台自行生成 wheel。
小结
源码树(source tree)、源码分发(source distribution)和 wheel 之间的区别:
- 源码树——包含在开发者的机器/存储仓上可用的所有项目文件(业务逻辑、测试、打包数据、CI 文件、IDE 文件、SVC 等),例如,请参见上面的示例项目。
- 源码分发——包含构建 wheel 所需的代码文件(业务逻辑+打包数据+通常还包括单元测试文件,用于校验构建;但是不包含开发者环境的内容,例如 CI/IDE/版本控制文件),格式:pugs-0.0 .1.tar.gz 。
- wheel——包含包的元数据和源码文件,被放到 site packages 文件夹,格式:pugs-0.0.1-py2.py3-NONE-any.whl 。
Charles PH 摄/Unsplash--hmmm
可在此阅读本系列的下一篇文章【7】,了解在安装软件包时会发生什么。谢谢阅读!
相关链接
[1] The state of Python Packaging: https://www.bernat.tech/pep-517-and-python-packaging/
[2] gaborbernat / pugs: https://github.com/gaborbernat/pugs
[3] setuptools: https://pypi.org/project/setuptools
[4] PEP-370: https://www.python.org/dev/peps/pep-0370/
[5] https://pypi.org: https://pypi.org/
[6] PEP-427: https://www.python.org/dev/peps/pep-0427/%23id14#id14
[7] 下一篇文章: https://www.bernat.tech/pep-517-518/
公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。