一、说明

在这篇博客中,我们将使用 Setuptools 创建一个源代码发行版和 python 包的轮子。在转向我们如何打包之前。我们先来了解一下什么是源分发和轮子文件。

二、早期使用 Setup 构建 Python 包

        软件包是指可用于解决特定用例的可用单元或库。对于打包,我们有不同的构建工具,比如在 Scala 中我们有 SBT,或者在 JAVA 中我们有 maven 等。同样,我们在 python 中使用 Setuptools 来创建发行版。Source Distribution (sdist) 和 Wheels (whl) 是在 python 中创建包分发的两种主要方式。

2.1  源分配 ( sdist )

        除了源代码之外,它还包含额外的文件,如 CI 脚本、测试用例等,这些文件不需要为最终用户和最少使用的方法提供。这将以形式分发包,为不同的平台创建不同格式的存档。默认格式在 Linux 上是 gzip 压缩的 tar 文件 (),在 Windows 上是 ZIP 文件。.tar.gz

2.2 车轮 (whl)

         此发行版包含安装所需的源文件和包元数据。(.whl)


setup.py:

  1. 此文件包含 setup() 函数,我们在其中提供项目的特定详细信息。这包含包名称、版本、需要包、作者、描述、分类器等信息。
  2. 它是用于运行与打包任务相关的各种命令的命令行界面。如果您想更深入地了解,只需运行 python setup.py — 帮助

python打包whl第三方库 python 打包wheel_项目结构



对于项目结构,我使用以下结构:

dummy_project/
|-- README
|-- setup.py
|-- example_pkg
|   |-- __init__.py
|   |-- calculate.py
|   |-- result.py
|-- tests
|-- |-- __init__.py
|-- |-- run.py
|-- |-- test.py

让我们创建最重要的文件 setup.py,它存在于项目目录的根目录中。

from setuptools import setup

setup(
    name = "dummy_project",
    version = "1.0.0",
    author = "dummy",
    author_email = "dummy@domain.com",
    description = ("This project helps to understand about setup.py."),
    url = "https://www.python.org/doc/",
    packages=['example_pkg'],
    python_requires='>=3.6.0'
)

现在,让我们使用以下命令创建分配

python setup.py sdist bdist_wheel

这将生成两个发行版。现在,根据您的用例,您可以将包推送到公共或私有 python 存储库。可以通过 pip 命令安装,如下所示:

pip install package_name

在下一篇博客中,我们将学习如何执行 python 项目的自动版本控制。

参考资料
Python Packaging User Guide



python打包whl第三方库 python 打包wheel_Python_02



DevOps的


        首先需要保证你有最新版的setuptools 和wheel

python -m pip install --user --upgrade setuptools wheel


三、使用Setuptools进行Python打包






本文将解释创建 Python 包的最佳实践。

它将涵盖:

  • 为什么我们要打包我们的代码
  • Python 打包的演变
  • Py脚手架
  • setup.cfg 中的有用配置
  • 构建并发布
  • 替代工具

python打包whl第三方库 python 打包wheel_Python_03



摄影:Jiawei Zhao ,来自Unsplash

四、为什么我们要打包我们的代码?

Python 的主要优势之一是其庞大的包生态系统。开发人员只需简单的 pip 安装和导入即可轻松获得所有内容。包是一种使代码可重用且易于共享的方法。编写库而不是集合脚本会迫使您考虑设计和清晰的界面。

很多时候,一些代码被隐藏在某个存储库中。需要克隆它,安装需求,一般来说,使用起来不太方便。我们应该努力做得更好。

五、Python 打包的演变

5.1 requiements.txt

大多数新手都会从 和requirements.txt一些松散耦合的脚本开始他们的 Python 之旅。

# 项目结构
├──requirements.txt 
├──script_1.py 
└──script_2.py
#requirements.txt
 numpy 
pandas==1.5.3
# 安装
pip install -rrequirements.txt

        它的简单性是主要吸引力,并且共享这些脚本可能很容易。只要这些脚本能够独立运行就可以了。但将它们集成到另一个项目中会很不方便。通常发生的情况是,人们会将您的脚本复制粘贴到他们的项目中,复制您的代码并进行自己的修改。

        如果你想让你的代码更加可重用,你需要编写一个库,打包并发布它。这样,每个人都可以直接安装并导入你的库。

        Python 打包因没有完整的文档而臭名昭著。做事没有确定的方法。即使是官方文档(没有中心位置)也可能包含过时的信息。随着时间的推移,许多不同的工作流程和工具已经发展。我不得不说,近年来文档已经有了很大的改进。那么如何创建一个包呢?

5.2 Distutils 与 setuptools

        人们可能经常会遇到这两个名字,一开始这让我很困惑。但这只是一个历史性的事情,Python 最初是作为distutils一个内置库,它是用于打包等的工具的集合。为了改进它,它setuptools被创建为一个可 pip 安装的包。今天,distutils根据PEP 632的决定,从 Python 3.10 版本开始正式弃用。

5.3 src 布局与平面布局

        在开始项目之前,您必须对项目结构做出决定。当我开始使用 Python 时,弄清楚项目结构是一个重大挑战。有多种方法可以做到这一点。当我阅读这篇文章时,这对我很有帮助,它确定了两个主要问题。有 src 布局和平面布局。


python打包whl第三方库 python 打包wheel_python打包whl第三方库_04

python打包whl第三方库 python 打包wheel_项目结构_05

src 与平面布局


        两者之间的区别在于库代码的位置。在 src-layout 中,所有库代码都存储在 src 文件夹中,与所有其他样板文件(例如测试和文档)分开。这种清晰的分离对于自动化和包发现很有用。例如,您不会意外地发送样板代码。这样做的缺点是,在开发时,您需要进行可编辑的安装,因为 src 文件夹的额外误导,最终会出现在您的导入路径中。(例如仅import src.mypkg有效)

        在平面结构中,您可以直接执行代码(例如使用python -m package.script),因为所有内容都位于顶级文件夹中。但您需要更加小心地发现包。

        两种解决方案都是有效的,对于我们下面的示例,我们将专门使用 src-layout。

5.4 setup.py

        使用 setuptools,您可以拥有的最小设置是一个setup.py. 我们可以将依赖项移至requirements.txt参数中install_requires。此外,我们需要传递更多参数来考虑 src 样式布局。使用packagespackages_dir,我们src从导入路径中删除并指定所有包都位于src有关更多详细信息,您可以阅读我关于包发现的文章。

# Project structure
├── setup.py
└── src
    └── tutorial
        ├── __init__.py
        ├── script_1.py
        └── script_2.py
# setup.py
import setuptools
from setuptools import setup

setup(
    name="python_example",
    install_requires=[
        "numpy",
        "pandas==1.5.3",
    ],
    package_dir={"": "src"},
    packages = setuptools.find_packages(
        where='src',
    ),
)
# Editable install
pip install -e .

这个设置可以完成工作。我们已经可以构建并发布这个包了。但是……这不是最佳实践。我们将在接下来的部分中找出原因。

5.5 setup.py + setup.cfg

        一个问题setup.py是它是可执行的。要读取元数据,setuptools 需要执行此脚本。对于自动化工具来说,它过于复杂且缓慢。例如,如果依赖解析器可以从机器可读的配置文件中读取依赖项,那么它会比执行 python 代码快得多。这就是setup.cfg引入的原因。

# Project structure
├── setup.cfg
├── setup.py
└── src
    └── tutorial
        ├── __init__.py
        ├── script_1.py
        └── script_2.py
# setup.py
from setuptools import setup

setup()

所有可用的参数都setup.py可以在 中定义setup.cfg。当可用时,setup.py将自动读取, 。setup.cfg

# setup.cfg
[metadata]
name = python_example

[options]
packages = find_namespace:
include_package_data = True
package_dir =
    =src

[options.packages.find]
where = src

        声明式setup.cfg更容易自动解析,并且不需要运行 Python 代码。在setup.py. 我的建议是,您从 开始setup.cfg,如果您发现达到了其局限性,则可以回退到setup.pypybind11是一个您必须依赖的示例来setup.py构建您的 C++ 代码。

5.6 构建依赖问题

        还有一个问题,又回到了setup.py。这是构建依赖问题:

  • setup.py如果不知道它的依赖关系就无法运行
  • 不运行就无法知道依赖关系setup.py

        这就是引入 PEP 518 的原因,用他们的话说:

[...] 当项目选择使用 setuptools 时,使用像 setup.py 这样的可执行文件就成为一个问题。在不知道其依赖项的情况下,您无法执行 setup.py 文件,但目前没有标准方法可以自动了解这些依赖项,而无需执行存储该信息的 setup.py 文件。这是一个第 22 条军规:如果不知道文件本身的内容,文件就无法运行,除非您运行该文件,否则无法以编程方式知道文件的内容。

资料来源: https: //peps.python.org/pep-0518/#rationale

解决方案是创建一个名为pyproject.toml.

5.7 setup.py + setup.cfg + pyproject.toml

# 项目结构</span>
├── pyproject.toml 
├── setup.cfg 
├── setup.py 
└── src 
    └──tutorial 
        ├── __init__.py 
        ├── script_1.py 
        └── script_2.py
[build-system]
requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"]
build-backend = "setuptools.build_meta"

        我们pyproject.toml可以指定构建系统的版本。在安装包的过程中,它将创建一个隔离的环境并安装正确的setuptools版本。



python打包whl第三方库 python 打包wheel_开发语言_06



        pip install 根据 pyproject.toml 创建一个隔离的 venv

  setuptools只是众多构建后端之一。PEP 518标准化了构建后端的接口,使社区能够构建替代后端。



python打包whl第三方库 python 打包wheel_python打包whl第三方库_07



前端和后端,来源:https ://peps.python.org/pep-0517/

5.8 使用项目生成器:PyScaffold

        现在我们知道在启动项目时需要创建一堆文件。幸运的是,我们不必记住每一个细节。我们可以使用像PyScaffold这样的项目生成器。它为您建立了完美的项目结构并包含所有最佳实践。维护人员将了解最新的打包开发并相应地更新项目模板。

您可以简单地安装它:

pip install pyscaffold
putup python_example



python打包whl第三方库 python 打包wheel_python_08



pyscaffold 生成的文件

六、setup.cfg 中的有用配置

        现在我们已经有了正确的项目结构,我们将讨论一些经常使用的配置。

6.1 包裹名字

        您setup.cfg可以为您的包命名。这将确定 PyPI 下显示的名称,您将使用pip install <name>.



python打包whl第三方库 python 打包wheel_python打包whl第三方库_09



        安装程序.cfg

        我们必须在模块集合(Python 文件)中区分 PyPI 包和 Python 包。您用于导入代码的名称与 setup.cfg 名称无关,而是由 src 文件夹下的项目结构决定。



python打包whl第三方库 python 打包wheel_开发语言_10



具有多个包的项目结构



python打包whl第三方库 python 打包wheel_开发语言_11



导入名称

一个突出的例子是 opencv。您可以使用pip install opencv-pythonbut来安装它import cv2

6.2 版本



python打包whl第三方库 python 打包wheel_python_12



安装程序.cfg

版本控制遵循语义版本控制。简而言之:

  • 主要:不兼容的 API 更改
  • MINOR:添加功能,向后兼容
  • PATCH:向后兼容的错误修复

但大多数项目并没有严格遵守这些规则,您应该从用户的角度考虑什么是有意义的。

        最佳实践是,您应该在每次更改时升级版本,并且不应覆盖已经发布的包。您可能会引入重大更改,而其他人的构建管道将会中断。但他们将无法恢复到以前的工作版本,因为您已经覆盖了它。这将阻止他们,直到他们将代码库调整为您的新界面。对于第三方来说,恢复到以前的版本来修复他们的版本会更容易。然后,他们可以毫无压力地调整他们的代码以适应您的更改。

6.3 版本 - PyScaffold 陷阱

        当我使用 PyScaffold 时,有一个小烦恼我总是需要解决。默认项目模板将尝试从版本控制系统推断版本。这假设您每个存储库都有一个包。如果您有一个包含多个包的 monorepo,您将遇到此错误:



python打包whl第三方库 python 打包wheel_python打包whl第三方库_13



        运行: putup <package>



python打包whl第三方库 python 打包wheel_python打包whl第三方库_14



        运行: pip install -e 。

        要修复它,请按照下列步骤操作:

putup <package_name> --force



python打包whl第三方库 python 打包wheel_python打包whl第三方库_15



去掉红色部分



python打包whl第三方库 python 打包wheel_python_16



去掉红色部分

6.4 安装要求



python打包whl第三方库 python 打包wheel_开发语言_17



安装程序.cfg

您可以在 下添加您的依赖项install_requires。您可以用来<=, ~=, >=, ==指定版本范围。

最佳实践是添加所有直接依赖项,即您在代码中导入的包。例如,numpy即使它带有,您也应该添加,因为如果您决定从项目中opencv删除,您的代码将会中断,因为不再有。opencvnumpy

另一个最佳实践是不要对依赖项版本过于严格。您可能倾向于使用最新版本的库,但由于存在许多依赖项,用户可能难以满足约束。如果有足够的依赖项,依赖项解析器将无法找到所有依赖项都兼容的星座。

6.5 额外要求

        太多的依赖,你很快就会发现自己陷入了依赖地狱。有时依赖项对于核心功能来说是可选的。就像运行时不需要的测试库一样。extras_requires您可以在名称下定义可选的依赖项。



python打包whl第三方库 python 打包wheel_Python_18



安装程序.cfg



python打包whl第三方库 python 打包wheel_python打包whl第三方库_19



安装额外的依赖项



python打包whl第三方库 python 打包wheel_python打包whl第三方库_20



可编辑安装的语法



python打包whl第三方库 python 打包wheel_Python_21



Horovod是一个分布式学习框架,支持多种深度学习框架。安装所有框架是不可能的,因此您可以选择一个带有extras_require.

如果您只使用包中的单个函数,则应该考虑自己实现它以减少依赖项的数量。

6.6 命名空间包

        命名空间包是本身不包含代码的包。它们是一种将包分组到一个命名空间下的方法。如果您想以同一名称发布多个包,这非常有用。它可以是您的伞式项目或组织的名称。这样,您还可以在不同存储库中处理不同的包,但仍然以相同的名称发布。例如,Azure有很多可以单独安装的包,但你可以像这样导入它们:



python打包whl第三方库 python 打包wheel_开发语言_22



        天蓝色命名空间包

        PyScaffold 已经使用该指令默认创建了此行为find_namespace。您只需将包放在命名空间文件夹下即可。使用find_namespace,__init__.py不再需要,因为空文件夹也将被解析为包。



python打包whl第三方库 python 打包wheel_python_23



安装程序.cfg



python打包whl第三方库 python 打包wheel_项目结构_24



项目结构



python打包whl第三方库 python 打包wheel_项目结构_25



导入路径

6.7 入口点

        另一种有用的设置是entry_points. 它将在您的命令行中安装一个可执行文件。这对于创建 CLI 很有用。使用 PyScaffold,您只需取消注释骨架示例即可测试此功能。与 结合使用click,您可以立即创建自己的 CLI。



python打包whl第三方库 python 打包wheel_python_26



安装程序.cfg



python打包whl第三方库 python 打包wheel_Python_27



命令行中可用的脚本

七、构建和发布

        当您完成包后,您应该考虑将其发布到 PyPI,要么发布到公共索引,要么考虑为您的内部解决方案使用公司托管的索引。

使用该build包来构建您的包:



python打包whl第三方库 python 打包wheel_项目结构_28



构建命令

将创建两个工件:whlsdist



python打包whl第三方库 python 打包wheel_python打包whl第三方库_29



构建工件

Wheel (whl) 是包装代码的首选格式。您可以使用 检查内容unzip -l <package>.whl。轮子包含的文件格式可以通过将内容解压缩到站点包中来轻松安装。有关轮子的更多信息,请参见PEP 427

源代码分发版 (sdist) 更像是项目的副本。也可以安装它,但它更多地用于归档目的。

您可能会遇到一些仍然setup.py手动运行的教程。但这种行为已被弃用。



python打包whl第三方库 python 打包wheel_开发语言_30



已弃用的构建和安装方式

最后一步是使用twine发布到 PyPI:

<span style="color:rgba(0, 0, 0, 0.8)"><span style="background-color:#ffffff"><span style="background-color:#f9f9f9"><span style="color:#242424">pip install twine 
twine 上传 dist/*</span></span></span></span>

八、替代工具

我们之前介绍的是基本的包装堆栈:

  • 前端:
  • 后端:设置工具
  • 构建:构建
  • 发布: 麻绳

由于 Python 打包的坎坷历史,许多其他工具被创建来提供更好的体验。有很多,所以我只介绍一些值得注意的。

8.1 cookie切割机

如果您对 PyScaffold 项目模板不满意,可以使用Cookiecutter。Cookiecutter 是一个人们可以定义自己的可重用项目模板的项目。这个想法是创建一个cookiecutter,一旦你拥有它,你就可以用它来切出新的cookie(项目)。您可以使用其他人的模板,也可以作为高级用户创建您的个人模板。

8.2 Poetry(诗歌)

Poetry现在非常流行,它的灵感来自于 Rust 的 Cargo 包管理器。它取代了整个 pip、setuptools 等堆栈,提供了一个可以完成所有操作的命令行工具。

8.3 项目数据管理

        与诗歌类似,pdm 受到另一个包管理器的启发,即 npm。一个值得注意的功能是它支持PEP 582,这消除了 virtualenvs 的需要。对于 Python 来说,这是一个令人兴奋的新方向。也许根本不需要 virtualenvs 的概念。

更新: PEP 582 被拒绝。合理的理由是,影响模块搜索路径的方法已经太多了,与新用户获得的微小好处相比,再增加一种方法只会增加更多的复杂性。

九、最后的话

        包装的方法有很多种。我个人的建议是将 PyScaffold 与常规打包堆栈一起使用 - pip、setuptools、build、twine。如果您知道自己在做什么,请考虑 cookiecutter。或者,如果您想要获得更简化的体验并愿意进行一些实验,请选择 Poetry 和 PDM。我说的是实验性的,但它们已经很成熟并且在社区中受到好评。我们的梦想是有一天我们会有一种明确的做事方式,这样我们就不必再仔细比较不同的工具了。