pip 19.0 已经于 2019 年 1 月 22 日发布。在其功能列表中,最值得注意的是它现在支持 PEP-517,默认情况下是支持的,如果项目的根目录中有一个 pyproject.toml。该 PEP 于 2015 年创建,并于 2017 年被接受。尽管 pip 花了一段时间才实现它,但该版本及其后续问题却表明,很多人根本不熟悉它。
如果你想了解 Python 打包(packaging)生态的现状及将来如何演变,请继续阅读。
我大约在三年前加入了 Python 开源社区(尽管使用它已有 8 年之久)。从早期开始,我就听说 Python 打包有一点黑匣子的名声。它有很多未知的内容,人们通常只复制其它项目的构建配置文件,就使用上了。
在尝试更好地理解这个黑匣子,并对其进行改进的过程中,我已经成为了 virtualenv 和 tox 项目的维护者,偶尔也为 setuptools 和 pip 做些贡献。
我希望对这个主题进行详尽的(并希望是一个较高水平的)论述,并决定将其分为三个部分。在这第一篇文章中,我将对 Python 打包的工作方式及其所具有的打包类型进行大概介绍。
事先声明,我将主要关注 Python 官方的打包系统(即 pip、setuptools,因此没有 conda 或特定于操作系统的打包程序)。
一个示例项目
为了讲这个故事,我需要先讲讲如何分发 Python 软件包的故事;更具体地说,包的安装在过去是如何运作的,以及我们希望它在将来如何运作。
为了有一个具体的示例,让我介绍一下我的很棒的示例库:pugs 。
这个库相当简单:它只生成一个名为 pugs 的包,仅包含一个名为 logic 的模块。关于 pugs,你猜对了,logic 被用于生成随机的引号。
这是一个展现为源码树(source tree)的简单示例结构(可以在gaborbernat / pugs 里获得):
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)
有助于项目管理和维护的文件:
- 持续集成(azure-pipelines.yml)
版本控制(.git)
项目管理(例如潜在的 .github 文件夹)
Python3.7.2(v3.7.2:9a3ffc0492,Dec242018,02:44:43)[Clang6.0(clang-600.0.57)]ondarwinType"help","copyright","credits"or"license"formoreinformation.>>>importpugs>>>pugs.do_tell()"Anenlightenedpugknowshowtomakethebestofwhateverhehastoworkwith-APug'sGuidetoDating-GemmaCorrell"
Python 包的可用性
Python 怎么知道什么可用或不可用?简短的答案是,它不知道。至少不在前期知道。相反,它将尝试加载,并动态地检查是否可用。
它从哪里加载?有许多可能的位置,但是在大多数情况下,我们说的是从文件系统的文件夹中加载。这个文件夹在哪里呢?对于给定的模块,可以打印该模块的表示(representation)来找出:
>>>importpugs>>>pugs<module'pugs'from'/Users/bernat/Library/Python/3.7/lib/python/site-packages/pugs/__init__.py'>
你会发现文件夹的位置取决于:
软件包的类型(三方库或者标准库的内置/aka部分)
它是全局的或仅限于当前的用户
以及它是系统 Python 还是一个虚拟环境
但是一般来说,对于给定的 Python 解释器,可以通过打印出 sys.path 变量的内容,来找到可能的目录列表,例如在我的 MacOS 上:
>>>importsys>>>print('\n'.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),上传到可以被终端用户计算机访问的中央存储仓。
终端用户计算机使用一些安装程序来查找、下载和安装相关软件包。安装操作最终是在 site-packages 文件夹内,创建正确的目录结构和元数据。
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 中可以找到每个文件的详细说明。
我们如何从源码树中获得这两种类型的内容呢?我们面前有两条截然不同的路径:
从我们的源码树生成此目录结构和元数据,将其压缩为单个文件,然后将其发布到中央软件包存储仓。在这种情况下,安装程序必须下载软件包并将其解压到 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 。
原文链接:
https://www.bernat.tech/pep-517-and-python-packaging/