首页 > 编程语言 >Python 打包——过去、现在与未来

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

时间:2022-12-20 13:33:42浏览次数:50  
标签:Python setup py 过去 构建 build https 打包

英文 | ​​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 内部的工作原理不是很精通。

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_wheel​​​或​​python setup.py sdist​​ 命令。

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

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 遗留决策约束的打包工具。

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

Sarthak Dubey摄/Unsplash--更多 yay!

tox 和打包

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

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

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

[tox]
isolated_build = True

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

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

Python 打包——过去、现在与未来_发行版_04

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_05

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




标签:Python,setup,py,过去,构建,build,https,打包
From: https://blog.51cto.com/u_14244765/5954757

相关文章

  • 我的 2019 年 Python 文章榜单
    现在是2020年的第一天,我相信从昨天开始,各位的信息流里肯定充斥了各式各样的年度盘点/回顾/总结/记录之类的内容。虽然来得稍晚了,但我还是想给诸位送上这一篇文章。我将在......
  • Python 3 既是激进的又是克制的,这些提议被否决了
    [译]PEP3099--Python3中不会改变的事情导语:Python3.8已经发布了,引进了不少变更点。关于3.9预计引入的修改,也披露了一些。我们之前还关注过​​GIL的移除计划​​......
  • Python 为什么要保留显式的 self ?
    花下猫语:前两天,我偶然在一个(刘欣老师的“码农翻身”)里看到一篇主题,刘老师表示Python的类方法非要带个self,而不像其它语言那样隐藏起来,这让人很不爽。我对此也有同感。在......
  • 更好用的 Python 任务自动化工具:nox 官方教程
    英文|​​noxtutorial​​​译者|豌豆花下猫@Python猫Github地址:​​https://github.com/chinesehuazhou/nox_doc_cn​​声明:本翻译基于​​CCBY-NC-SA4.0​​授权协议......
  • Python 任务自动化工具:nox 的配置与 API
    英文|​​Configuration&API​​​译者|豌豆花下猫@Python猫Github地址:​​https://github.com/chinesehuazhou/nox_doc_cn​​声明:本翻译基于CCBY-NC-SA4.0授权协......
  • PYTHON - openpyxl (四)
    1.1字体Font类型:openpyxl.styles.fonts.Fontsheet["a1"].font=Font(name='字体',size=字号,...)name字体,中文字体前面加usize字号大小bold=True/False粗......
  • 用Python写一个一次性计算出加减乘除的运算小程序
    0x1前言跟上一个购物车一样的玩法,也都挺简单的,看标题意思就懂了,不做多介绍了0x2加减乘除的运算小程序importre#处理字符串中的符号defsign_exp(strvar):s......
  • python的time模块功能详解
    >>>importtime>>>dir(time)['__doc__','__name__','__package__','accept2dyear','altzone','asctime','clock','ctime','daylight','gmtime......
  • Python实现点阵字体读取与转换
    点阵字体是指根据文字的像素点来显示的字体,效果如下:使用Python读取并显示的过程如下: 根据中文字符获取GB2312编码通过GB2312编码计算该汉字在点阵字库中的区位和码位通过区......
  • 再谈Python多线程--避免GIL对性能的影响
    在Python中使用多线程,如果你对GIL本身没有一定的了解;那么很有可能你只是写出了正确的多线程代码,而并没有达到多线程的目的,甚至截然相反的效果。下面介绍了Python中GIL的作用......