赞
踩
上文说,如何用setup.py打包成egg包,继而又有find_package()函数用法,本篇专门介绍,如何用setuptools打包成不同的软件包。在这篇博客中,我们将使用 Setuptools 创建一个源代码发行版和 python 包的轮子。在转向我们如何打包之前。我们先来了解一下什么是源分发和轮子文件。
软件包是指可用于解决特定用例的可用单元或库。对于打包,我们有不同的构建工具,比如在 Scala 中我们有 SBT,或者在 JAVA 中我们有 maven 等。同样,我们在 python 中使用 Setuptools 来创建发行版。Source Distribution (sdist) 和 Wheels (whl) 是在 python 中创建包分发的两种主要方式。
除了源代码之外,它还包含额外的文件,如 CI 脚本、测试用例等,这些文件不需要为最终用户和最少使用的方法提供。这将以形式分发包,为不同的平台创建不同格式的存档。默认格式在 Linux 上是 gzip 压缩的 tar 文件 (),在 Windows 上是 ZIP 文件。.tar.gz
此发行版包含安装所需的源文件和包元数据。(.whl)
setup.py
:
对于项目结构,我使用以下结构:
- 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
首先需要保证你有最新版的setuptools
和wheel
python -m pip install --user --upgrade setuptools wheel
本文将解释创建 Python 包的最佳实践。
它将涵盖:
摄影:Jiawei Zhao ,来自Unsplash
Python 的主要优势之一是其庞大的包生态系统。开发人员只需简单的 pip 安装和导入即可轻松获得所有内容。包是一种使代码可重用且易于共享的方法。编写库而不是集合脚本会迫使您考虑设计和清晰的界面。
很多时候,一些代码被隐藏在某个存储库中。需要克隆它,安装需求,一般来说,使用起来不太方便。我们应该努力做得更好。
大多数新手都会从 和requirements.txt
一些松散耦合的脚本开始他们的 Python 之旅。
- # 项目结构
- ├──requirements.txt
- ├──script_1.py
- └──script_2.py
- #requirements.txt
- numpy
- pandas==1.5.3
- # 安装
- pip install -rrequirements.txt
它的简单性是主要吸引力,并且共享这些脚本可能很容易。只要这些脚本能够独立运行就可以了。但将它们集成到另一个项目中会很不方便。通常发生的情况是,人们会将您的脚本复制粘贴到他们的项目中,复制您的代码并进行自己的修改。
如果你想让你的代码更加可重用,你需要编写一个库,打包并发布它。这样,每个人都可以直接安装并导入你的库。
Python 打包因没有完整的文档而臭名昭著。做事没有确定的方法。即使是官方文档(没有中心位置)也可能包含过时的信息。随着时间的推移,许多不同的工作流程和工具已经发展。我不得不说,近年来文档已经有了很大的改进。那么如何创建一个包呢?
人们可能经常会遇到这两个名字,一开始这让我很困惑。但这只是一个历史性的事情,Python 最初是作为distutils
一个内置库,它是用于打包等的工具的集合。为了改进它,它setuptools
被创建为一个可 pip 安装的包。今天,distutils
根据PEP 632的决定,从 Python 3.10 版本开始正式弃用。
在开始项目之前,您必须对项目结构做出决定。当我开始使用 Python 时,弄清楚项目结构是一个重大挑战。有多种方法可以做到这一点。当我阅读这篇文章时,这对我很有帮助,它确定了两个主要问题。有 src 布局和平面布局。
src 与平面布局
两者之间的区别在于库代码的位置。在 src-layout 中,所有库代码都存储在 src 文件夹中,与所有其他样板文件(例如测试和文档)分开。这种清晰的分离对于自动化和包发现很有用。例如,您不会意外地发送样板代码。这样做的缺点是,在开发时,您需要进行可编辑的安装,因为 src 文件夹的额外误导,最终会出现在您的导入路径中。(例如仅import src.mypkg
有效)
在平面结构中,您可以直接执行代码(例如使用python -m package.script
),因为所有内容都位于顶级文件夹中。但您需要更加小心地发现包。
两种解决方案都是有效的,对于我们下面的示例,我们将专门使用 src-layout。
使用 setuptools,您可以拥有的最小设置是一个setup.py
. 我们可以将依赖项移至requirements.txt
参数中install_requires
。此外,我们需要传递更多参数来考虑 src 样式布局。使用packages
和packages_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 .
这个设置可以完成工作。我们已经可以构建并发布这个包了。但是……这不是最佳实践。我们将在接下来的部分中找出原因。
一个问题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.py
. pybind11
是一个您必须依赖的示例来setup.py
构建您的 C++ 代码。
还有一个问题,又回到了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
.
- # 项目结构</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
版本。
pip install 根据 pyproject.toml 创建一个隔离的 venv
setuptools
只是众多构建后端之一。PEP 518标准化了构建后端的接口,使社区能够构建替代后端。
前端和后端,来源:https ://peps.python.org/pep-0517/
现在我们知道在启动项目时需要创建一堆文件。幸运的是,我们不必记住每一个细节。我们可以使用像PyScaffold这样的项目生成器。它为您建立了完美的项目结构并包含所有最佳实践。维护人员将了解最新的打包开发并相应地更新项目模板。
您可以简单地安装它:
- pip install pyscaffold
- putup python_example
pyscaffold 生成的文件
现在我们已经有了正确的项目结构,我们将讨论一些经常使用的配置。
您setup.cfg
可以为您的包命名。这将确定 PyPI 下显示的名称,您将使用pip install <name>
.
安装程序.cfg
我们必须在模块集合(Python 文件)中区分 PyPI 包和 Python 包。您用于导入代码的名称与 setup.cfg 名称无关,而是由 src 文件夹下的项目结构决定。
具有多个包的项目结构
导入名称
一个突出的例子是 opencv。您可以使用pip install opencv-python
but来安装它import cv2
。
安装程序.cfg
版本控制遵循语义版本控制。简而言之:
但大多数项目并没有严格遵守这些规则,您应该从用户的角度考虑什么是有意义的。
最佳实践是,您应该在每次更改时升级版本,并且不应覆盖已经发布的包。您可能会引入重大更改,而其他人的构建管道将会中断。但他们将无法恢复到以前的工作版本,因为您已经覆盖了它。这将阻止他们,直到他们将代码库调整为您的新界面。对于第三方来说,恢复到以前的版本来修复他们的版本会更容易。然后,他们可以毫无压力地调整他们的代码以适应您的更改。
当我使用 PyScaffold 时,有一个小烦恼我总是需要解决。默认项目模板将尝试从版本控制系统推断版本。这假设您每个存储库都有一个包。如果您有一个包含多个包的 monorepo,您将遇到此错误:
运行: putup <package>
运行: pip install -e 。
要修复它,请按照下列步骤操作:
putup <package_name> --force
去掉红色部分
去掉红色部分
安装程序.cfg
您可以在 下添加您的依赖项install_requires
。您可以用来<=, ~=, >=, ==
指定版本范围。
最佳实践是添加所有直接依赖项,即您在代码中导入的包。例如,numpy
即使它带有,您也应该添加,因为如果您决定从项目中opencv
删除,您的代码将会中断,因为不再有。opencv
numpy
另一个最佳实践是不要对依赖项版本过于严格。您可能倾向于使用最新版本的库,但由于存在许多依赖项,用户可能难以满足约束。如果有足够的依赖项,依赖项解析器将无法找到所有依赖项都兼容的星座。
太多的依赖,你很快就会发现自己陷入了依赖地狱。有时依赖项对于核心功能来说是可选的。就像运行时不需要的测试库一样。extras_requires
您可以在名称下定义可选的依赖项。
安装程序.cfg
安装额外的依赖项
可编辑安装的语法
Horovod是一个分布式学习框架,支持多种深度学习框架。安装所有框架是不可能的,因此您可以选择一个带有extras_require
.
如果您只使用包中的单个函数,则应该考虑自己实现它以减少依赖项的数量。
命名空间包是本身不包含代码的包。它们是一种将包分组到一个命名空间下的方法。如果您想以同一名称发布多个包,这非常有用。它可以是您的伞式项目或组织的名称。这样,您还可以在不同存储库中处理不同的包,但仍然以相同的名称发布。例如,Azure有很多可以单独安装的包,但你可以像这样导入它们:
天蓝色命名空间包
PyScaffold 已经使用该指令默认创建了此行为find_namespace
。您只需将包放在命名空间文件夹下即可。使用find_namespace
,__init__.py
不再需要,因为空文件夹也将被解析为包。
安装程序.cfg
项目结构
导入路径
另一种有用的设置是entry_points
. 它将在您的命令行中安装一个可执行文件。这对于创建 CLI 很有用。使用 PyScaffold,您只需取消注释骨架示例即可测试此功能。与 结合使用click
,您可以立即创建自己的 CLI。
安装程序.cfg
命令行中可用的脚本
当您完成包后,您应该考虑将其发布到 PyPI,要么发布到公共索引,要么考虑为您的内部解决方案使用公司托管的索引。
使用该build
包来构建您的包:
构建命令
将创建两个工件:whl
和sdist
:
构建工件
Wheel (whl) 是包装代码的首选格式。您可以使用 检查内容unzip -l <package>.whl
。轮子包含的文件格式可以通过将内容解压缩到站点包中来轻松安装。有关轮子的更多信息,请参见PEP 427。
源代码分发版 (sdist) 更像是项目的副本。也可以安装它,但它更多地用于归档目的。
您可能会遇到一些仍然setup.py
手动运行的教程。但这种行为已被弃用。
已弃用的构建和安装方式
最后一步是使用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 打包的坎坷历史,许多其他工具被创建来提供更好的体验。有很多,所以我只介绍一些值得注意的。
如果您对 PyScaffold 项目模板不满意,可以使用Cookiecutter。Cookiecutter 是一个人们可以定义自己的可重用项目模板的项目。这个想法是创建一个cookiecutter,一旦你拥有它,你就可以用它来切出新的cookie(项目)。您可以使用其他人的模板,也可以作为高级用户创建您的个人模板。
Poetry现在非常流行,它的灵感来自于 Rust 的 Cargo 包管理器。它取代了整个 pip、setuptools 等堆栈,提供了一个可以完成所有操作的命令行工具。
与诗歌类似,pdm 受到另一个包管理器的启发,即 npm。一个值得注意的功能是它支持PEP 582,这消除了 virtualenvs 的需要。对于 Python 来说,这是一个令人兴奋的新方向。也许根本不需要 virtualenvs 的概念。
更新: PEP 582 被拒绝。合理的理由是,影响模块搜索路径的方法已经太多了,与新用户获得的微小好处相比,再增加一种方法只会增加更多的复杂性。在这里阅读更多详细信息:PEP 582 - Python local packages directory - #429 by frostming - Packaging - Discussions on Python.org
包装的方法有很多种。我个人的建议是将 PyScaffold 与常规打包堆栈一起使用 - pip、setuptools、build、twine。如果您知道自己在做什么,请考虑 cookiecutter。或者,如果您想要获得更简化的体验并愿意进行一些实验,请选择 Poetry 和 PDM。我说的是实验性的,但它们已经很成熟并且在社区中受到好评。我们的梦想是有一天我们会有一种明确的做事方式,这样我们就不必再仔细比较不同的工具了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。