前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python打包指南2021

Python打包指南2021

作者头像
岂不美哉Frost
发布2023-10-19 09:35:09
2910
发布2023-10-19 09:35:09
举报
文章被收录于专栏:Frost's BlogFrost's Blog

大家圣诞快乐,雕虫小技栏目又和大家见面了,谁让咱不会那些个屠龙之技,只好捉几个虫子玩玩了。 写这篇文章是因为过去的两年关于pip和 Python 包管理有几个重要的 PEP 发布,然而网上(中文世界)的打包发布教程很少有针对此的更新。 再加上我成为 PyPA 的成员已经尸位素餐快一年了,还是应该来做点贡献。

setup.py 真难写

似乎从有 Python 打包以来就有了setuptools这个库,你能搜到的教程,涉及打包发布的,都会让你编写那个可怕的setup.py。 不知道谁能完全掌握那个东西的写法,我到现在都还不太会。说几个常用的配置:

指定依赖和可选依赖

代码语言:javascript
复制
setup(
    install_requires=["flask", "flask-migrate", "sqlalchemy"],
    extras_require={"mysql": ["mysqlclient"], "pgsql": ["psycopg2"]}
)

注意那两个 key 分别是install_requiresextras_require,别写错了。此外,如果你需要根据条件增减依赖的话,不要用

代码语言:javascript
复制
INSTALL_REQUIRES = ["flask"]
if sys.platform == "win32":
    INSTALL_REQUIRES.append("pywin32")
setup(install_requires=INSTALL_REQUIRES)

而应该使用Environment Markers

代码语言:javascript
复制
INSTALL_REQUIRES = [
    "flask",
    "pywin32; sys_platform == 'win32'"
]
setup(install_requires=INSTALL_REQUIRES)

发布可执行程序到/bin

代码语言:javascript
复制
setup(
    entry_points={
        "console_scripts": ["mybin=mypackage.main:cli"]
    }
)

或者 ini 写法

代码语言:javascript
复制
setup(
    entry_points="""[console_scripts]
    mybin = mypackage.main:cli
    """
)

任选其一。

包含 data 文件

代码语言:javascript
复制
setup(
    include_package_data=True    # 从MANIFEST.in中读取配置
)

或者

代码语言:javascript
复制
setup(
    package_data={"": ["*.json"]}  # 包含所有json文件
)

指定源代码结构,如果你使用的是src/存放包的源码这种项目结构,可以:

代码语言:javascript
复制
setup(
    package_dir={"": "src"}
)

打包上传和安装

打包

好了,这个万恶的setup.py我已经写好了,咱要发布 PyPI 了。第一步,打包成可分发的文件:

代码语言:javascript
复制
$ python setup.py sdist bdist_wheel --universal

这条命令会同时生成源代码包(Source Distribution),和二进制包(Binary Distribution)。当然,大部分的 Python 发布包中并不真的包含二进制, 只是沿用了软件工程中的一般叫法。其中bdist_wheel生成的二进制包是 wheel 格式(需要安装wheel才能打包),--universal的意思是这个二进制包对所有 支持的 Python 版本和 ABI 都适用,「 一处打包,到处使用」,生成的文件名类似:my_package-0.1.0-py3-none-any.whl。如果你包中有 C 扩展, 也就是打包出来的 wheel 会真的有二进制文件时就不能加这个 flag 了,这时生成的文件名类似:my_package-0.1.0-cp38-cp38-win_amd64.whl。 这个文件名不是乱来的,是要遵循一定规则,下载器能直接从这个文件名获得这个包的基本信息:

20201225160452
20201225160452

上传

可能有老的教程,让你直接用python setup.py sdist bdist_wheel register upload打包上传一步到位,这个方式已经过时了不推荐使用。正确的方法应该用twine工具:

代码语言:javascript
复制
$ twine upload dist/*

如果你要把上传放到 CI 里自动执行,最好生成一个 token 来使用,访问 https://pypi.org/manage/account/token/ 按提示生成一个 token,使用的时候只要用命令指定下用户名和密码:

代码语言:javascript
复制
twine upload --username __token__ --password ${{ secrets.PYPI_TOKEN }} dist/*

安装

把包上传到 PyPI 以后,pip install my-package的时候是怎么安装的呢?

  1. 访问https://pypi.org/simple/my-package,解析所有链接
  2. 若是 whl 文件,判断是否与当前 Python 版本、ABI、平台适配,加入到候选列表
  3. <a>标签中读取data-requires-python属性,判断是否与当前 Python 版本兼容,加入候选列表
  4. 若是源代码包,直接加入候选列表

最终在候选列表中优先选择 whl 文件为待安装的包,将包下载到本地,候选包的选择可以由pip install--only-binary--no-binary选项控制。

现在准备安装了,如果待安装的是 whl,那就非常简单,直接解压(whl 文件是一种 zip 格式),放到目标目录即可,解压后产生的文件除了代码或二进制以外,还会包含一个my_package-0.1.0.dist-info/目录,包含这个包的元数据信息,比如有哪些文件、文件 hash 值、entry_points 等等。

如果待安装的文件是源代码包,那么需要把这个压缩包解压到一个临时目录,根据包指定的方式编译构建,生成 whl 文件,再用 whl 安装同样的方法放到目标目录中。而这个指定的编译方式,在 PEP 517 提案之前,是调用python setup.py install命令。在 PEP 517 发布之后,则由 PEP 517 的 build backend 控制。

注意,在 PEP 517 提案之后的今天,永远不要再用python setup.py installpython setup.py build这两种方式安装和构建包了,所有的 PyPI 上的包,都必须通过 wheel 格式安装,如果没有 wheel 包的,则必须提供符合 PyPA 规范的源码包,经过 PEP 517 构建为 wheel 格式之后再安装,pip install <package>背后就是这样做的。为了更好地掌握,你也可以分开执行这两个步骤:

代码语言:javascript
复制
$ pip wheel foo-0.1.0.tar.gz -d dist/
$ pip install dist/foo-0.1.0-py3-none-any.whl

PEP 517 的目的就是通过统一协议抽象这两个过程,使它能后端无关化,PyPA 针对两者也分开有独立的工具:pypa/buildpradyunsg/installer

setuptools 不再是唯一的选择

PEP 517 的内容简单来说,就是在项目根目录下的pyproject.toml定义了两个特殊属性1

代码语言:javascript
复制
[build-system]
requires = ["setuptools >= 40.8.0", "wheel"]
build-backend = "setuptools.build_meta:__legacy__"

上面这个就是setuptools的 PEP 517 的配置,这样可以让老的项目,能直接用 PEP 517 的方式构建。如果你的项目中并没有pyproject.toml文件,pip能自动填充为此缺省配置。其中requires意为这个 backend 依赖的包列表,build-backend则为 backend 的具体位置。这个 backend 需要实现几个约定的接口:

  1. get_requires_for_build_wheel,构建 wheel 需要的依赖列表,这个一般没有特殊要求都是空
  2. get_requires_for_build_sdist,构建 sdist 需要的依赖列表,同上
  3. prepare_metadata_for_build_wheel,生成一个 wheel 要用的dist-info/文件夹
  4. build_wheel,生成 wheel 文件
  5. build_sdist,生成 sdist 文件

有了这些接口,pip以及其他可能的 frontend 就能从源代码构建一个 wheel 出来。因此,pyproject.toml必须被包含在源代码包中。

有了 PEP 517 的协议规范以后,backend 和 frontend 就能自由组合,不再是非setuptools不可了,实现了 PEP 517 的 backend 有:

所以我可以不用写 setup.py 了

setup.py作为一个元数据的定义格式是有问题的:

  1. 必须由 Python 运行,无法静态解析
  2. 由于第 1 点,有注入恶意代码的操作可行性

所以需要指定一个元数据的配置格式,这个格式规范最近也定下来了,它就是 PEP 621,也是使用pyproject.toml来定义的。而且,PDM已经支持这个配置格式了,仅此一家。


阅读链接

Footnotes

  1. 其实还有第三个属性backend-path,当你的 backend 是在本地时使用。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-12-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • setup.py 真难写
  • 打包上传和安装
    • 打包
      • 上传
        • 安装
        • setuptools 不再是唯一的选择
        • 所以我可以不用写 setup.py 了
        • 阅读链接
        • Footnotes
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档