创建和发布自己的 Python 包到 PyPI 上

像 Java 可发布包到 Maven 仓库,NodeJS 发布包到 NPM 一样,我们也可以创建自己的 Python 包并发布到 PyPI 仓库中去。或者内部使用的包,只须发布到私有的 Nexus 服务器上。

本文中的例子将创建一个 Python 包 bounded-executor, 并发布到 PyPI 上。为什么创建这个包呢?原因是直接用 Python 的 ThreadPoolExecutor 或  ProcessPoolExecutor 来提交任务的话,任务的等待队列是没有边界的,这就会造成因提交任务过快而使得内存爆满。本包最为合适的名称应该是 bounded-pool-executor, 可是名字已被他人使用,但此外的实现有所不同,ThreadPoolExecutor 用 Queue(maxsize) 来控制,而 ProcessPoolExceutor 用 BoundedSemaphore 来控制。

我们以经典的 Python 工程目录结构为例,构建的核心是执行 setup.py 中的 setup 函数,由此来理解 setup 的最主要配置与关键命令做了些什么。这样有助于我们理解其他的 Python 包管理工具的底层行为,从中我们可以对比 poetry 的 build, install, 和 publish 命令。

打包发布的准备

  1. setuptools: 这个无需安装,在用 Python -m venv 创建一个虚拟环境后就有了
  2. pip install wheel:如果要生成 wheels 格式的包就须安装
  3. pip install twine: 方便发布包到 PyPI 上, python setup.py ... upload 不再使用
  4. PyPI 上注册一个帐号

项目的结构

bounded-executor
├── README.rst
├── bounded_executor
│       ├── __init__.py
│       └── executors.py
└── setup.py

关键就是 setup.py 文件

该项目 bounded-executor 的核心实现代码不是这里的重点。主要的配置是 setup.py 文件,它调用 setuptools.setup 函数

注:distutils.core.setup 和 setuptools.setup 两个 setup 函数目前都可用,推荐使用 setuptools 中的 setup 函数,见 setuptools vs. distutils: why is distutils still a thing?

包依赖的其他第三方包用 install_requires 配置,如

测试依赖用 test_requirements 配置,格式与 install_requires  一致。

说明:

  1. 调用 setup 函数只需填上基本的 nameversion 两个参数也能构建,发布成功,但尽量填上更详细的包相关的信息。
  2. 发布到 PyPI 上的版本可以被删除,但被删除的版本号不能再重复使用
  3. long_description 是显示在项目页面(https://pypi.org/project/bounded-executor/)主体部位中的内容,可以用 Markdown 或 RST,默认的 long_description_content_typetext/x-rst, 如果使用 Markdown  格式, long_description_content_type 须设置为 text/markdown
  4. 也可以用 setup.cfg 搭配 setup.py 来使用,比如在 setup.cfg 中的 [meatadata] 可填上项目相关的信息,在 setup.py 中只需一个无参的 setup() 函数调用,则 setup() 的信息将会从 setup.cfg 中来。多加一个 setup.cfg 还是显得有点多余,setup() 函数中的参数名描述的足够清楚。参见 Configuring setup() using setup.cfg files. setup.cfg 中有一个方便之处就是可用 attr 或 file 来读取其他文件中的信息,如 long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst

配置了 setup() 后,接下来基本就是围绕着 setup.py 中的 setup() 函数调用。

可以用 python setup.py --help 查看它所支持的一些参数,不过似乎 setuptools 一直没有更新 --help 的输出,它所显示的信息远远不够,更完整的 setup() 函数所支持的参数请参考 setuptools setup() Keywords

以及后面将要用到的其他一系列的命令,用 --help-commands 预览一下

各式打包相关的命令

开发模式下安装包

先用命令 python -m venv ~/test-venv 创建一个虚拟环境,并 source ~/test-venv/bin/activate 应用它

$ python setup.py develop

它会在当前目录下生成一个目录 bounded_executor.egg-info, 其中有四个文件 PKG-INFO, SOURCES.txt, dependency_links.txttop_level.txt。还在虚拟环境中生成了两个文件

~/test-venv/lib/python3.9/site-packages/bounded-executor.egg-link,内容为

/Users/yanbin/Workspaces/github/bounded-executor
.

~/test-venv/lib/python3.9/site-packages/easy-install.pth, 内容为

/Users/yanbin/Workspaces/github/bounded-executor

后一个文件才是关键,因为它是在 site-packages 目录下的 pth 文件,这就意味着使用同一个虚拟环境的其他项目会直接通过访问 /Users/yanbin/Workspaces/github/bounded-executor 源文件的方式来使用其中的代码。

python setup.py build: 把待打包的文件放到 build/ 目录下

python setup.py sdist: 在 dist 目录下生成以源文件形式发布的包 bounded_executor-0.0.3.tar.gz

$ tar -tf dist/bounded_executor-0.0.3.tar.gz
bounded_executor-0.0.3/
bounded_executor-0.0.3/PKG-INFO
bounded_executor-0.0.3/README.rst
bounded_executor-0.0.3/bounded_executor/
bounded_executor-0.0.3/bounded_executor/__init__.py
bounded_executor-0.0.3/bounded_executor/executors.py
bounded_executor-0.0.3/bounded_executor.egg-info/
bounded_executor-0.0.3/bounded_executor.egg-info/PKG-INFO
bounded_executor-0.0.3/bounded_executor.egg-info/SOURCES.txt
bounded_executor-0.0.3/bounded_executor.egg-info/dependency_links.txt
bounded_executor-0.0.3/bounded_executor.egg-info/top_level.txt
bounded_executor-0.0.3/setup.cfg
bounded_executor-0.0.3/setup.py

python setup.py bdist: 在 dist 目录下生成以二进制形式发布的包,如 bounded_executor-0.0.3.macosx-11-x86_64.tar.gz,它是平台相关的,相对于前面会把 bounded_executor/__pycache__ 目录放到包中

python setup.py bdist_egg: 在 dist 目录中生成 bounded_executor-0.0.3-py3.9.egg,现在不推荐用 egg 包。它也是一个 zip 文件

python setup.py bdist_wheel: 在 dist 目录中管理层成 bounded_executor-0.0.3-py3-none-any.whl, 它也是 zip 包

egg 包文件有点老,一般我们只会生成 tar.gz 和 whl 文件, 用

$ python setup.py sdist bdist_wheel

同时生成  bounded_executor-0.0.3.tar.gzbounded_executor-0.0.3-py3-none-any.whl 文件,我们后面要发布到 PyPI 的包可以是其中的一个。

其他打包的形式还有 bdist_dump, bdist_rpm, 和 bdist_wininst

在正式进入发布包之前顺带看下 python setup.py install 发生了什么, 首先它在 dist 目录中生成了 bounded_executor-0.0.3-py3.9.egg 包,并安装(拷贝)到了虚拟环境的 sit-packages 中了,本例为

~/test-venv/lib/python3.9/site-packages/bounded_executor-0.0.3-py3.9.egg

并在 ~/test-venv/lib/python3.9/site-packages/easy-install.pth 文件中加上一行

./bounded_executor-0.0.3-py3.9.egg

表明已加到了该虚拟环境的 sys.path 中去了,所以开发中还是用 python setup.py develop 就行了,改了源代码后能直接生效,不需要再次 python setup.py install

发布包到 PyPI

终于来到了最后一步,要把前面生成的包上传到 PyPI 上,我们无须执行 python setup.py register 进行事先注册包,直接上传就行。早先上传包是用 python setup.py <sdist|bdist_wheel> upload 的方式,这种方式也不推荐使用(可能已无法使用),因为它采用 HTTP  传输,见 Uploading your Project to PyPI

直接用 twine 命令发布包,我们在运行 twine 时会每次提示输入用户名和密码,如果可以把 PyPI 帐号信息存在  ~/.pypirc 文件中。配置明文的帐号和密码

或在 pypi.org 中为帐号设置一个 token, 然后在  ~/.pypirc 中的 username 就是 __token__, password 就是那个 token

在前面用了  python setup.py sdistpython setup.py bdist_wheel 在 dist 目录中生成了相应的包文件后,先运行 twine check  确保包是有效的

$ twine check dist/bounded_executor-0.0.3.2-py3-none-any.whl

然后上传

$ twine upload dist/bounded_executor-0.0.3.2-py3-none-any.whl

上传成功后就能在 https://pypi.org/project/bounded-executor/0.0.3/ 看到该包的信息,并且随时可用 pip install bounded-executor 来安装使用。

对于生成的 wheel 包是完全一样的操作

$ python setup.py sdist
$ twine upload dist/bounded_executor-0.0.3.tar.gz

或者用 twine upload dist/* 上传 dist 目录所有包,这在严格的构建发布过程中是不推荐的。

对于 tar.gz 还是 wheel, 如果是纯 Python 的包,我们推荐使用  wheel 包。

如果要发布到私有的 Nexus 服务器上可以用

$ twine upload --repository-url=https://nexus.example.com/repository/pypi-hosted/ dist/bounded_executor-0.0.3-py3-none-any.whl

然后依照提示输入用户名和密码就能上传了。

同时发布了 tar.gz 和 wheel 包情况

相同版本的包,我们可以用 twine 发布 tar.gz 和  wheel 包,同时发布了两种类型的包进在用 pip install 时可以留意到它安装的是哪种类型的包,对于 tar.gz 源码包 pip install 下载后仍然要通过  setup.py 创建相应的  whl 包再进行安装使用。所以对于纯 Python 的包做成 universal 的 whl 包发布的话,安装效率最高,同时它实质也包含了源代码。对于使用到了其他的语言(如  C/C++, Rust) 写的 Python 包,可以用  tar.gz 发布,但 pip install 安装时需要有相应的环境进行编译,或者发布为 whl 包,这时候就要考虑为多种操作系统平台与 CPU 架构生成多个 whl 包。

每个版本下有多少个文件,什么类型的文件在 PyPI 中会列出来。

pip 指定仓库安装包

pip install 命令默认从 PyPI 仓库安装包,如果包上传到了别的仓库,或公司的私有仓库,应该如何用 pip install 来安装呢?那就是

$ pip install -i https://nexus.example.com/repository/pypi-hosted bounded_executor

-i, --index-url <url>, 也可以用 --extra-index-url <url>, 特别是 requirements.txt 文件同时含有 PyPI 和私有仓库中包,就应该

$ pip --extra-index-url https://nexus.example.com/repository/pypi-hosted -r requirements.txt

总结

最后总结一下打包发布的过程:

  1. pip install twine wheel
  2. 配置好 setup.py 文件
  3. python setup.py bdist_wheel
  4. 配置 ~/.pypirc, 如果不想每次 upload 输入用户名/密码
  5. twine upload dist/my-package-*.whl

链接:

  1. Python 库打包分发(setup.py 编写)简易指南
  2. 测试开发技术 实战教程: 如何将自己的 Python 包发布到 PyPI 上
  3. TheadPoolExecutor with a bounded queue in Python
  4. Python cachetools

本文链接 https://yanbin.blog/create-publish-your-own-pypi-python-package/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments