英文 | The state of Python Packaging [1]
原作 | BERNAT GABOR
译者 | 豌豆花下猫
如果你想了解 Python 打包 (packaging) 生态的现状及将来如何演变, 请继续阅读. 我们希望, 即使上述提到的 Python 增强提案(译注: 即 PEP, 关于 PEP 的介绍, 请阅读 这篇文章 https://mp.weixin.qq.com/s/oRoBxZ2-IyuPOf_MWyKZyw ), 如今可能会引起一些不愉快, 但从长远来看, 我们将从中受益.
我大约在三年前加入了 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 https://github.com/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.YAML
这里有四类独特的内容:
我们的 pugs 包在用户机器的解释器上能用, 意味着什么? 在理想情况下, 一旦启动解释器, 用户应该能够 import 它, 并调用其中的函数:
业务逻辑代码(src 文件夹中的内容)
测试代码(tests 文件夹和 tox.INI)
包代码和元数据(setup.py,setup.cfg,LICENSE.txt,README.rst-- 请注意, 我们如今使用的是事实上的标准打包工具 https://pypi.org/project/setuptools [3] )
有助于项目管理和维护的文件:
持续集成(azure-pipelines.YAML)
版本控制(.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 https://www.python.org/dev/peps/pep-0370/ [4] )
以及它是系统 Python 还是一个虚拟环境
但是一般来说, 对于给定的 Python 解释器, 可以通过打印出 sys.path 变量的内容, 来找到可能的目录列表, 例如在我的 MacOS 上:
- >>> import sys
- >>> 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), 上传到可以被终端用户计算机访问的中央存储仓(通常为 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] 中可以找到每个文件的详细说明.
我们如何从源码树中获得这两种类型的内容呢? 我们面前有两条截然不同的路径:
wheel
这两个方法的区别主要在于包的编译 / 构建操作发生在哪里: 在开发者的计算机上还是在终端用户的计算机上. 如果它发生在开发者的一边(例如在 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
可在此阅读本系列的 下一篇文章 https://www.bernat.tech/pep-517-518/ [7] , 了解在安装软件包时会发生什么. 谢谢阅读!
来源: http://www.tuicool.com/articles/6veq2ym