创建一个包
https://python-packaging-zh.readthedocs.io/zh_CN/latest/index.html
setup.py
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
安装:pip install -e .
Python 打包一开始可能有些难以理解。其主要原因是不了解创建 Python 包的正确工具。
不管怎样,一旦创建了第一个包,就会会发现它并不像看起来那么难。
此外,熟悉正确且先进的打包工具也很有帮助。
你即使对将代码开源分发不感兴趣,但也应该知道如何创建包。
知道如何创建自己的包,可以让你深入了解打包生态系统,并且有助于你使用 PyPI 上可用的第三方代码。
此外,将你的闭源项目或其组件变成源代码发行包,有助于你在不同的环境中部署代码。
Python 打包工具的混乱状态
Python 打包曾经在很长一段时间内处于混乱不堪的状态,人们花了很多年才使得这一主题重新变得有组织。
一切都从 1998 年引入的distutils
包开始,随后在 2003 年setuptools
对其进行改进。
这两个项目开启了一段漫长而又纠结的故事,故事包括派生(fork)、替代项目与完全重新编写,都想要彻底修复 Python 的打包生态系统。
不幸的是,大部分尝试都没有成功。效果恰恰相反。
每个想要取代setuptools
或distutils
的新项目只是给打包工具十分混乱的状态添乱而已。
有些派生被合并回它们的祖先中(例如 setuptools 派生的 distribute),但有些则被弃用(distutils2)
幸运的是,这种状态正在逐步改变。
成立了一个叫做 Python Packaging Authority(PyPA)的组织,将秩序和组织性带回打包生态系统中。
PyPA 维护的 Python 打包用户指南(Python Packaging User Guide,https://packaging.python.org/)是关于最新打包工具和最佳实践的权威信息来源。
由于PyPA的存在,Python打包的现状
PyPA 除了提供一份权威的打包指南之外,还维护着打包项目与新的官方打包的标准化过程。
请参阅:https://github.com/pypa
这些项目中国呢最有名的是:
- pip
- virtualenv
- twine
- warehouse
注意,大部分项目都是在这个组织之外开始的,只是作为一个成熟且广泛使用的解决方案迁移到 PyPA 的赞助下。
工具推荐
在 Python 打包用户指南中,推荐包的创建与分发的工具如下:
- 使用 setuptools 来定义项目并创建源代码发行版(source distributions)
- 使用 wheel 而不是 egg 来创建构建发行版(built distributions)
- 使用 twine 向 PyPI 上传包的发行版
项目配置
很显然,组织大型应用的代码的最简单方法是将其分成几个包。
这使得代码更加简单,也更容易理解、维护和修改。
这样也使每个包的可复用性最大化。它们的作用就像组件一样。
setup.py
对于一个需要被分发的包来说,其根目录包含一个 setup.py 脚本。
它定义了 distutils 模块中描述的所有元数据,并将其合并为标准的 setup() 函数调用的参数。
虽然 distutils 是一个标准库模块,但建议你使用 setuptools 包来代替,它对标准的 distuils 做了一些改进。
因为,这个文件的最少内容如下:
1 2 3 4 5 |
|
name 给出了包的全名。
该脚本提供了一些命令,你可以用--help-commands
选项列出以下这些命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
Standard commands
(标准命令)是 distutils 提供的内置明林,而Extra commands
(额外命令)则是由诸如 setuptools 这样的第三方包或任何其他定义并注册一个新命令的包所创建的。
由另一个包注册的一个额外命令就是 wheel 包提供的 bdist_wheel
setup.cfg
setup.cfg
文件包含setup.py
脚本命令的默认选项。
如果构建和分发包的过程更加复杂,并且需要向setup.py
命令中传入许多可选参数,那么这个文件非常有用。
你可以按照项目将这些默认参数保存在代码中。
这将使你的分发流程独立于项目之外,也能够让包的构建方式与向用户和其他团队成员的分发变得透明。
setup.cfg
文件的语法与内置configparser
模块提供的语法相同,因此它类似于常见的 Microsoft Windows INI 文件。
下面是安装配置文件的示例,提供了 global、sdist 和 bdist_wheel 命令的默认值,代码如下:
1 2 3 4 5 6 7 8 |
|
这个配置示例可以确保源代码发行版总是以两种格式创建(ZIP 和 TAR),并且构建 wheel 发行版将被创建为通用 wheel(与 Python 版本无关)。
此外,由于全局 quiet 开关,每个命令的大部分输出都将被阻止。
注意,这只是为了便于说明,默认阻止每个命令的输出可能并不是一个合理的选择。
MANIFEST.in
使用 sdist 命令构建发行版时,distutils 将浏览包的目录,查找需要包含在存档中的文件。
distutils 将包含:
- py_modules、packages 和 scripts 选项隐含的所有 Python 源文件
- ext_modules 选项列出的所有 C 源文件
最重要的元数据
除了被分发包的名称和版本之外,setup 可以接收的最重要的参数包括。
description
: 包含描述包的几句话。long_description
: 包含完整说明,可以使用 reStructuredText 格式keywords
: 定义包的关键字列表author
: 作者的姓名和组织author_email
联系人电子邮件地址url
: 项目的 URL。license
: 许可证(GPL、LGPL 等)packages
: 包中所有名称的列表;setuptools 提供了一个名为find_packages
的小函数来计算它namespace_packages
: 命令空间包的列表
常见模式
对于没有经验的开发者来说,创建一个用于分发的包可能是一项乏味的任务。
如果不考虑元数据可能在项目其他部分找到的事实,setuptools 或 distuitls 在 setup() 函数调用中接受的大多数元数据都可以手动输入,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这么做当然可行,但从长远来看很难维护,并且未来可能会出现错误和不一致。
setuptools 和 distuitls 都不能从项目源代码中自动提取各种元数据信息,因此你需要自己提供这些信息。
在 Python 社区中有一些常见模式剋解决最常见的问题,例如依赖管理、包含版本/自述文件等。
至少应该知道其中一些模式,因为它们非常流行,已经被看做一种打包惯例(packaging idioms)
(1) 自动包含包中的版本字符串
PEP 440(版本标识和依赖规范,Version Identification and Dependency Specifiaction)文档规定了版本和依赖规范的标准。
这是一份很长的文档,包含已接受的版本规范方案和 Python 打包工具中应该如何做版本匹配和比较。
如果你正在使用或打算使用一种复杂的项目版本编号方案,那么一定要阅读这份文档。
如果你使用的是一种简单方案,其中包含用点分开的一个、两个、三个或更多的数字,那么可以不必阅读 PEP 440。
目前来看,管理兼容性未来变化的最佳方法,就是正确使用语义化版本(Semantic Versioning semver)的版本号。
它是一个广为接受的标准,用仅包含 3 个数字的版本标识符来标记代码的变化范围。
它还给出了如何处理弃用的方法建议。下面是摘录 semver 官网的摘要。
版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
- 主版本号(MAJOR): 当你做了不兼容的 API 修改
- 次版本号(MINOR): 当你做了向后兼容的功能性新增。
- 修订号(PATCH): 当你做了向后兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修改号”的后面,作为延伸。
另一个问题是将包或模块的版本标识符包含在什么位置。
PEP 396(模块版本号,Module Version Numbers)正好解决了这个问题。
注意,这份文档只是信息性的(informational),并且状态为延期(deferred),所以它并不是标准路径(standards track)的一部分。
不管怎样,它描述的内容现在似乎成了事实上的标准。
根据 PEP 396,如果一个包或模块要指定一个版本,那么应该将其包含在包的根目录(__init__.py
)或模块文件的__version__
属性中。
另一个事实上的标准是,也要将包括版本元组的 VERSION 属性包含其中。
这有助于用户编写兼容代码,因此如果版本方案足够简单的话,这样的版本元组容易比较。
因此,PyPI 上的很多包都遵循这两个标准。
它们的__init__.py
文件包含如下所示的版本属性,如下所示:
1 2 3 4 |
|
延期的 PEP 396 的另一个建议是,在 distutils 的 setup() 函数中提供的版本应该从__version__
派生,反之亦然。
Python 打包用户指南为单一来源的项目版本提供了多种模式,每一种都有自己的优点和局限性。
我个人最喜欢相当长的,并没有包含在 PyPA 的指南中,但它的优点是仅限制 setup.py 脚本的复杂度。
这个样板假定,版本标识符由包的__init__
模块的VERSION
属性给出,并且提取出这一数据包含在 setup() 调用中。
下面是某个虚构的包的 setup.py 脚本中的片段,其中使用了以下的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
(2) 管理依赖
许多项目需要安装和/或使用一些外部包。
如果依赖列表很长的话,就会出现一个问题:如何管理依赖?
在大多数情况下答案很简单。不要过度设计(over-engineer)问题。
保持简单,并在 setup.py 脚本中明确提供依赖列表,代码如下:
1 2 3 4 5 6 7 |
|
有些 Python 开发者喜欢使用 requirements.txt 文件来追踪包的依赖列表。
在某些情况下,你可能会找到这么做的原因,但在大多数情况下,这是项目代码没有正确打包的时代遗留问题。
无论如何,即使像 Celery 这样著名的项目也仍然坚持使用这一约定。
因此,如果你不愿意改变习惯或者不知何故被迫使用 requirements.txt 文件,那么至少要将其做对。
下面是从 requirements.txt 文件读取依赖列表的常见做法之一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
自定义 setup 命令
添加非代码的其他文件
https://python-packaging-zh.readthedocs.io/zh_CN/latest/non-code-files.html
通常我们的包都需要一下不是python代码的文件, 例如图片, 数据, 文档等等. 为了让setuptools正确处理这些文件, 我们需要特别定义一下这些文件.
我们需要在 MANIFEST.in
中指定这些文件, MANIFEST.in 提供了一个文件清单, 使用相对路径或是绝对路径指出打包时需要包含的特殊文件.:
1 2 3 |
|
为了让在安装的时候这些特殊文件能被复制到 site-packages 下的文件夹中, 需要在setup()
添加参数include_package_data=True
.
添加在安装包中的文件(例如计算需要的数据文件)应该放在Python模块的文件夹里面(例如 funniest/funniest/data.json
). 在加载这些文件的时候, 使用相对路径再加上 __file__
变量.
上传包
https://packaging.python.org/tutorials/packaging-projects/
1 2 |
|
This command should output a lot of text and once completed should generate two files in the dist directory:
1 2 3 |
|
1 2 3 4 5 |
|
执行twine upload dist/*
然后输入 pypi 的账号密码
更新包
首先必须要更新项目版本号
1 2 3 |
|