英文 | 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进阶之旅”:https://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进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。

Python 打包——过去、现在与未来的更多相关文章

  1. Python 打包和发布方法汇总

    以下主要Python打包汇总,作为学习备份: 一.使用python内部基础工具包Distutils打包程序 1. 在打包之前需要做的就是配置好安装脚本,一般为setup.py文件: 示例(setup. ...

  2. python打包成exe

    目前有三种方法可以实现python打包成exe,分别为 py2exe Pyinstaller cx_Freeze 其中没有一个是完美的 1.py2exe的话不支持egg类型的python库 2.Pyi ...

  3. Python打包工具setuptools的使用

    将我们写的Python程序发布成包后,可以使其能够安装使用. 在项目上测试的时候,某些情况下,可以将Python打包,然后上传到测试服务器,安装测试. setuptools是常用的打包工具. 一个简单 ...

  4. 反编译python打包的exe文件

    目录 1.前言 2.使用环境 3.还原过程 4.号外 5.exe文件和所用到的反编译工具 6.参考 7.去签名(补漏) 前言 拿到了利用驱动人生进行传播的病毒样本,发现是python打包成的exe文件 ...

  5. Python打包—Pyinstaller

    2018-09-27 21:12:05   一 前言 在windows平台学习python的过程中,你肯定会遇到需要把.py脚本打包成.exe的情形,如此,至少有两方面的好处:第一,你的代码保密性更好 ...

  6. Python 会是我们的未来吗?

    Python 热度激增 根据 Stack Overflow 的一项调查显示,Python 不仅在专业领域的使用率得到增长,在普通开发上的使用率也有所提升,有 40% 的受访者表示他们现在正在使用 Py ...

  7. 跟Python打包相关的一些文章

    6 things I learned about setuptools Python 101: easy_install or how to create eggs « The Mouse Vs. T ...

  8. Python 打包中 setpy.py settuptools pbr 的了解

    背景 nova服务构建失败,报错: 'tests_require' must be a string or list of strings containing valid project/versi ...

  9. Python打包文件夹的方法小结(zip,tar,tar.gz等)

    本文实例讲述了Python打包文件夹的方法.分享给大家供大家参考,具体如下: 一.zip ? 1 2 3 4 5 6 7 8 9 10 11 import os, zipfile #打包目录为zip文 ...

随机推荐

  1. oracle函数 last_day(d1)

    [功能]:返回日期d1所在月份最后一天的日期. [参数]:d1,日期型 [返回]:日期 [示例]select sysdate,last_day(sysdate)  hz from dual; 返回:2 ...

  2. CF986F Oppa Funcan Style Remastered

    CF986F Oppa Funcan Style Remastered 不错的图论转化题! 题目首先转化成:能否用若干个k的非1因数的和=n 其次,因数太多,由于只是可行性,不妨直接都用质因子来填充! ...

  3. Getting started with the basics of programming exercises_2

    1.编写简单power函数 #include<stdio.h> int power(int m, int n); // test power function int main(void) ...

  4. Python xrange() 函数

    描述 xrange() 函数用法与 range 完全相同,所不同的是生成的不是一个数组,而是一个生成器. 语法 xrange 语法: xrange(stop) xrange(start, stop[, ...

  5. 2018-10-19-Nuget-通过-dotnet-命令行发布

    title author date CreateTime categories Nuget 通过 dotnet 命令行发布 lindexi 2018-10-19 09:15:53 +0800 2018 ...

  6. Laravel 服务提供者实例教程 —— 创建 Service Provider 测试实例

    从某种意义上说,服务提供者有点类似HTTP控制器,HTTP控制器用于为相关路由注册提供统一管理,而服务提供者用于为相关服务容器提供统一绑定场所,此外服务提供者还可以做一些初始化启动操作.Laravel ...

  7. Spark1.6.1 MLlib 特征抽取和变换

    Spark1.6.1 MLlib 特征抽取和变换 1 TF-IDF TF-IDF是一种特征向量化方法,这种方法多用于文本挖掘,通过算法可以反应出词在语料库中某个文档中的重要性.文档中词记为t,文档记为 ...

  8. js随即数字random实现div点击更换背景色

    需求:点击按钮随机给盒子换背景色 用到的知识点:Math.random    Math.round 文章地址 https://www.cnblogs.com/sandraryan/ <!DOCT ...

  9. H3C 多路径网络中环路产生过程(1)

  10. 12174 - Shuffle——[滑动窗口]

    You are listening to your music collection using the shuffle function to keep the music surprising. ...