Python 打包——过去、现在与未来

1,203 阅读11分钟

英文 | Python packaging - Past, Present, Future【1】

原作 | BERNAT GABOR

译者 | 豌豆花下猫

声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。

你是否想过在运行 pip install 时究竟发生了什么?这篇文章将给你一个关于过去所涉及的步骤的详细综述,以及它是如何随着 PEP-517 和 PEP-518 的采用而改变的。

前一篇文章中,我描述了如何做到安装三种类型的内容:源码树(source tree)、源发行版(source distribution)和 wheel。只有最后两种类型会被上传到 PyPi 中央存储仓,但你也可以获得源码树(例如,通过为 pip 加入 git 协议)。与其它类型相比,wheel 的优点是不需要在用户机器上进行任何构建操作;只需要下载和提取。

构建 Python 包

现在可以独立出构建的环境(用户或开发者的机器),但你仍然需要构建包(sdist 或 wheel)。为了做到这一点,你需要一些适当的构建器。在过去,对第三方包的需求很早就表现出来了。

遵循内置电池的原则,在 2000 年的 Python 1.6 中,distutils【2】包被添加进 Python 标准库中。它引入了包含构建逻辑的setup.py 文件的概念,并通过python setup.py 命令触发。

它允许用户将代码打包成库,但没有声明(declaration)及自动安装依赖库等功能。而且,它的升级周期直接与核心解释器的发布周期绑定。

setuptools 于 2004 年创建,它构建在 distutils 之上,并扩展了其它优秀的特性。它很快变得非常流行,以至于大多数 Python 安装包开始将其与核心解释器一起提供。

在那个时候,所有的包都是源发行版。wheel 分发方式出现得很晚,是在 2014 年。distutils 是在只有少数非常精通打包的人的时候创建的。因此它是非常灵活和命令式的(imperative),你写一个 Python 脚本,可以修改包生成过程中的每一步。

但这样做的缺点是,它一点也不容易学习和理解。随着 Python 的流行,这开始成为一个越来越严重的问题,因为有越来越多的用户对 Python 内部的工作原理不是很精通。

Charles PH 摄/Unsplash--ehhh

构建依赖项

关于安装一个源发行版,pip 主要做了以下工作:

  1. 找到这个包
  2. 下载源发行版并提取它
  3. 在提取的文件夹上运行python setup.py install(进行构建+安装)

开发者运行python setup.py sdist 生成分发包,运行python setup.py upload 上传到中央存储仓(上传命令在 2013 年被弃用了,因为有 twine【3】工具,更主要是因为 upload 使用了不安全的 HTTP 连接,而且上传命令会做一次新的构建,也就不允许最终用户在实际上传之前检测(inspect)生成的包)。

当 pip 运行python setup.py install时,它使用 Python 解释器来安装包。因此,构建操作可以访问该解释器中已经存在的所有三方包。最值得注意的是,它完全使用了安装在主机 Python 解释器上的 setuptools 版本。如果一个包使用了 setuptools 的新版本特性,那么完成安装的唯一方法就是首先更新已安装的 setuptools。

如果新版本包含了能破坏其它包的 bug,就会导致出问题。在用户无法更改已安装包的系统上,这尤其麻烦。当构建器(例如 setuptools)希望使用其它辅助包(例如 cython)时,这也是个问题。

如果缺少构建器的辅助,通常会抛出导包失败的错误:

File "setup_build.py", line 99, in run
    from Cython.Build import cythonize
ImportError: No module named Cython.Build

在开发者们这边,没办法提供此类构建依赖项。而对于用户这边,则需要预先安装所有的包构建依赖,即使他们不会在运行时使用到。为了解决这个问题, PEP-518【4】被创建了。

其思想是,与其将主机的 Python 与其当前安装的构建包一起使用,不如给软件包提供一种能力,令其清楚地说明其构建操作所需的内容。另外,与其在主机 Python 上提供此功能,我们是创建了一个独立的 Python(类似某种虚拟环境)来运行打包。

python setup.py install 现在可以:

  1. 创建一个临时文件夹
  2. 创建一个隔离的(从三方库的 site packages 中)Python 环境 python -m virtualenv our_build_env,让我们将这个 Python 可执行文件称为python_isolated
  3. 安装构建的依赖项
  4. 通过python_isolated setup.py bdist_wheel,生成一个用于安装的 wheel
  5. 提取 wheel 到 Python 的 site packages 文件夹

有了这个,我们可以安装依赖于cython 的包,但不必在运行的 Python 环境中实际安装cython。指定构建依赖项的文件与方法的是pyproject.toml元数据文件:

[build-system]
requires = [
    "setuptools >= 40.8.0",
    "wheel >= 0.30.0",
    "cython >= 0.29.4",
]

此外,它还允许打包者指定他们需要的最小版本,而借助用户机器上的 pip,可以轻易地找出这些版本。

当在开发者的机器上生成源发行版或 wheel 时,也可以使用相同的机制。当一个人调用pip wheel . --no-deps命令时,该命令会自动在后台创建一个包含构建依赖项的独立 Python,然后在该环境中调用python setup.py bdist_wheelpython setup.py sdist 命令。

Bruce Galpin摄/Unsplash--yay!

多样的打包工具

但这里还有一个问题。请注意,所有这些操作仍然须通过 20 年前引入的机制,即执行setup.py。整个生态系统仍然构建在 distutils 和 setuptools 的接口基础之上,由于试图保持向后兼容性,没法作太大的变更。

此外,在打包过程中执行用户端 Python 代码是危险的,这可能会导致经验较少的用户难以调试的细微错误。命令式的(imperative)构建系统在 20 年前对于灵活性来说非常重要,当时我们还不知道所有的情况,但是现在我们已经认识清楚了,很可能可以为不同的情况创建出非常健壮和简单的包构建器。

引用 Paul Ganssle【5】(setuptools 与 dateutil 的维护者)的话:

理想情况下,默认选项应该是一个声明式的(declarative)构建配置,适用于 99% 的情况,再提供一个退回到命令式系统的选项,供真正需要灵活性时使用。在这情况下,如果你发现还需要选择用命令式的构建,那么我们可以认为出现了坏味道代码。

setup.py 的最大的问题是大多数人是声明式地使用它,所以当他们用命令式时,往往会将 bug 引入到构建系统。一个这样的例子:如果你有一个 Python2.7 的依赖项,你可能会试图有条件地在 setup.py 中指定 sys.version,但 sys.version 仅指的是执行构建的解释器;相反,你应该对需求项使用声明式的环境标记…

在 2015 年的引入的flit【6】已经证明了这一假设的正确性。它已经成为许多 Python 新手最喜欢的打包工具,因为它可以确保新用户避免很多这样的麻烦。然而,要达到这个目的,flit 必须再次构建在 distutils/setuptools 之上,这使得它的实现非常关键,并且代码仓出现相当多的垫片层(例如,它仍然为源发行版生成 setup.py 文件)。

现在是时候把它从这些束缚中解放出来了,同时也鼓励其他人构建自己的打包工具来简化打包,是时候让 setup.py 成为例外而不是默认的了。setuptools 计划提供【7】一个用户专用的setup.cfg 接口来起带头作用,当一个 PEP-517 系统就位时,在大多数情况下,你应该选择它而不是使用 setup.py。

为了不把所有东西都绑定到 setuptools 和 distutils 上,并使后端的构建变得便利, PEP-517【8】被创建了。它将构建器分成后端和前端。前端提供了一个隔离的 Python 环境,满足所有声明的构建依赖项;后端提供了钩子,被前端从其隔离环境中调用,以生成源发行版或者 wheel。

此外,我们不再通过 setup.py 文件或命令与后端通信,而是使用了 Python 模块和函数。所有后端的打包必须提供一个 Python 对象 API,至少实现 build_wheel【9】和 build_sdist【10】两个方法。该 API 对象是通过 pyproject.toml 文件指定的,使用build-backend 键值:

[build-system]
requires = ["flit"]
build-backend = "flit.api:main"

上述代码对于前端意味着,你可以通过在隔离的 Python 环境中运行它来控制后端:

import flit.api
backend = flit.api.main

# build wheel via 
backend.build_wheel()

# build source distribution via
backend.build_sdist()

由后端决定要在哪里和怎样公开自己的官方 API:

  1. flit【11】通过flit.buildapi实现
  2. setuptools【12】提供了两种变体:setuptools.build_meta(后面会解释原因)
  3. poetry【13】通过poetry.masonry.api实现

因为这些,我们就拥有了不再受 distutils 遗留决策约束的打包工具。

Sarthak Dubey摄/Unsplash--更多 yay!

tox 和打包

tox 是一个测试工具【14】,大多数项目使用它来确保某个包在多个 Python 解释器上的版本兼容性。它还可以轻松地创建 Python 环境,在里面安装被监测的包,从而更快地复现问题。

为了能够测试一个包,它首先需要构建一个源发行版。虽然 PEP-518 和 PEP-517 都带有好的意图,但是在某些情况下,启用它们可能会破坏打包过程。因此,当 tox 在 3.3.0 版本中添加隔离构建时,决定暂时不默认启用它。你需要手动启用它(可能会在今年晚些时候——2019 年的版本 4 中默认启用)。

一旦你指定了一个pyproject.toml ,写了适当的requiresbuild-backend,你需要启用tox.ini 中的isolated_build标志:

[tox]
isolated_build = True

在此之后,在打包过程中【15】,tox 将在独立的 Python 环境中为每个 PEP-518 提供构建依赖项,来构建源发行版,并调用 PEP-517 所述的构建后端。

若不启用该功能,tox 将使用老方法构建源发行版,也就是使用安装了 tox 的解释器来调用python setup.py sdist命令。

Matthew Henry摄/Unsplash--这里没有免费的午餐呢!

小结

Python 打包官方希望所有这些都是有意义的,并因此拥有一个更用户友好的、防错的(error proof )和健壮的构建。这些标准的规范是在 2015 年至 2017 年的长期主题中写作并争论出来的。这两个提案(PEP-517/518)被认为是足够好的,可以获得最大的收益,但是一些不太主流的场景可能会被忽略。

如果你的情况是被忽略的,不要担心,如果我们认为必要的话,PEP 在任何时候都是拥抱改进意见的。在本系列的下一篇文章中【16】,我将讨论社区在发布这两个 PEP 时碰撞到的一些痛点。这些都是我们应该吸取的教训,并且表明着我们仍有一些工作要做。还不是一切都完美,但我们正在变得更好。如果你可以帮帮忙,就加入打包社区吧,让我们一起把事情做得更好!

附1:勘误

前一篇文章中,source distribution 被译成“源码分发”,但它还有一个更被人采用的译法是“源发行版”,为了便于接受,所以本文已作修改。翻译匆忙,如有错误,欢迎读者指正!万分感谢!PS:后续若有修正,会在知乎专栏修改,请关注“Python进阶之旅”:zhuanlan.zhihu.com/pythonCat

附2:相关链接

[1] Python packaging - Past, Present, Future: https://www.bernat.tech/pep-517-518/

[2] distutils: https://packaging.python.org/key_projects/#distutils

[3] twine: https://pypi.org/project/twine/

[4] PEP-518 : https://www.python.org/dev/peps/pep-0518/

[5] Paul Ganssle: https://twitter.com/pganssle

[6] flit: https://pypi.org/project/flit/

[7] 计划提供: https://github.com/pypa/setuptools/pull/1675

[8] PEP-517: https://www.python.org/dev/peps/pep-0517/

[9] build_wheel: https://www.python.org/dev/peps/pep-0517/#build-wheel

[10] build_sdist: https://www.python.org/dev/peps/pep-0517/#id9

[11] flit: https://flit.readthedocs.io/en/latest/

[12] setuptools: https://setuptools.readthedocs.io/en/latest/history.html#v40-8-0

[13] poetry: https://poetry.eustace.io/docs/pyproject/#poetry-and-pep-517

[14] 测试工具: https://tox.readthedocs.io/en/latest/

[15] 打包过程中: https://tox.readthedocs.io/en/latest/#system-overview

[16] 下一篇文章中: https://www.bernat.tech/growing-pain/

公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。