1 前言
当我们开发一个Python
库或者封装了一套代码逻辑后,我们会纠结于如何在多个项目中应用。
如果采用最笨的办法,也就是代码复用,那就可能会面临以下问题:
- 当源码有bug时,需要手动替换其他的库
- 项目A为了实现自己的需求,修改了库,项目B是否需要同步?
在C++项目中,如果采取源码复用,往往可以使用
submodule
配合makefile
或CMake
实现库的顺序编译和引用,但如果库中同时包含了多种语言实现,那其中的Python
代码目录往往是较深的,引用起来非常不好看。 于是通用的做法就是将库封装成whl
文件,必要时上传到Pypi
中做到共享和版本控制。
2 步骤
2.1 创建项目
或许可以更简单点
已经有大神封装了库的模板,完全可以参考该模板的形式构建:navdeep-G/setup.py: 📦 A Human’s Ultimate Guide to setup.py. (github.com)
我们为此准备一个简单的测试项目:ZhengqiaoWang/JDistributer (github.com)
这个项目的Python
部分是开源的,C++
部分是闭源的,所以看起来好像是多种语言的目录结构,但实际上只有Python
代码。我们看一下整体目录结构:
.
├── CMakeLists.txt
├── LICENSE
├── README.md
├── examples
├── include
│ └── jdistributer
├── python
│ ├── JDistributer
│ │ ├── __init__.py
│ │ ├── adapter
│ │ │ └── mq
│ │ │ └── redis_adapter.py
│ │ ├── common_define.py
│ │ ├── noreply_consumer.py
│ │ ├── noreply_producer.py
│ │ ├── reply_consumer.py
│ │ ├── reply_group_consumer.py
│ │ └── reply_producer.py
│ ├── example
│ │ └── test.py
│ ├── setup.py
│ └── ut
│ ├── config.py
│ ├── test_group_reply.py
│ ├── test_no_reply.py
│ └── test_reply.py
└── ChangeLog
9 directories, 18 files
看起来有很多啊,实际上我们主要需要关注的只有以下几部分:
.
├── LICENSE # 协议
├── README.md # 帮助介绍
├── python # Python库
│ ├── JDistributer # 代码目录
│ ├── example # 示例目录
│ ├── setup.py # 核心的setup.py
│ └── ut # 单元测试目录
└── ChangeLog # 放变更记录的,版本号从这里面取
当然,如果你是纯Python
库的话,可以将python
目录内的东西移出来,变成这样的:
.
├── LICENSE # 协议
├── README.md # 帮助介绍
├── JDistributer # 代码目录
├── example # 示例目录
├── setup.py # 核心的setup.py
└── ut # 单元测试目录
└── ChangeLog # 放变更记录的,版本号从这里面取
根据需要进行调整,具体差异主要体现在setup.py
里的目录结构上,我仍然以JDistributer
项目的目录结构做示例,在与一般纯Python
项目中的不同之处我会标注出来。
2.2 安装依赖
通过命令
pip3 install setuptools wheel twine
安装我们需要的依赖,其中
- setuptools:用于打包
- wheel:用于打包成
whl
文件 - twine:用于上传到
Pypi
2.3 创建setup.py
在我们创建好目录结构后,我们就可以修改setup.py
了。模板可以参考setup.py/setup.py at master · navdeep-G/setup.py (github.com),我在它的基础上进行了修改ZhengqiaoWang/setup.py: 📦 A Human’s Ultimate Guide to setup.py. (github.com):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# NOTE 如果你想用`upload`功能,则需要在环境里安装`twine`
import io
import os
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
here = os.path.join(
os.path.dirname(os.path.abspath(__file__)), ".."
) # NOTE 这里因为我的基本路径是`setup.py`文件的上一层,因此额外往上走了一级
# 包的基本信息.
NAME = "JDistributer"
DESCRIPTION = "JDistributer Demo"
URL = "https://github.com/ZhengqiaoWang/JDistributer"
EMAIL = "me@zhengqiao.wang"
AUTHOR = "ZhengqiaoWang"
REQUIRES_PYTHON = ">=3.6.0"
# 代码库运行时的依赖
REQUIRED = [
"redis",
"hiredis",
]
# 可选安装什么依赖
EXTRAS = {
# 'fancy feature': ['django'],
}
# 将ChangeLog第一行读成VERSION
try:
with io.open(os.path.join(here, "ChangeLog")) as f:
VERSION = f.readline().strip("\n").strip()
except FileNotFoundError:
VERSION = "0.1.0" # 如果没有ChangeLog,则在这里设置版本
# 下面的基本上不怎么动,除非你要修改协议
# 将README.md文件读成long-description
try:
with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
long_description = "\n" + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
about = {}
about["__version__"] = VERSION
class UploadCommand(Command):
"""Support setup.py upload."""
description = "Build and publish the package."
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print("\033[1m{0}\033[0m".format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status("Removing previous builds…")
rmtree(os.path.join(here, "dist"))
except OSError:
pass
self.status("Building Source and Wheel (universal) distribution…")
os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable))
self.status("Uploading the package to PyPI via Twine…")
os.system("twine upload dist/*")
self.status("Pushing git tags…")
os.system("git tag v{0}".format(about["__version__"]))
os.system("git push --tags")
sys.exit()
# Where the magic happens:
setup(
name=NAME,
version=about["__version__"],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type="text/markdown",
author=AUTHOR,
author_email=EMAIL,
python_requires=REQUIRES_PYTHON,
url=URL,
packages=find_packages(
exclude=["tests", "*.tests", "*.tests.*", "tests.*", "ut", "example"]
),
# If your package is a single module, use this instead of 'packages':
# py_modules=['mypackage'],
# entry_points={
# 'console_scripts': ['mycli=mymodule:cli'],
# },
install_requires=REQUIRED,
extras_require=EXTRAS,
include_package_data=True,
license="MIT",
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
# $ setup.py publish support.
cmdclass={
"upload": UploadCommand,
},
)
2.3.1 修改ChangeLog
如果你打算使用ChangeLog
而不是手动指定版本号,那么你就需要在根目录创建一个ChangeLog
文件,并写成以下形式:
0.0.2
20240102
Hello World
0.0.1
20240101
DEMO
这样就会自动读取第一行0.0.2
作为版本号。
2.4 打包与调试
我们准备好了后,就可以先手动试一试了。
我们先进入python
目录
cd JDistributer/python
2.4.1 测试
使用命令
python3 setup.py test
可以对现在的setup.py
进行测试,当提示ok
时,说明写的没问题。
2.4.2 构建
使用命令
python3 setup.py sdist bdist_wheel --universal
会在目录下创建一个dist
目录,里面有打包生成的文件。需要注意的是,文件与上面构建命令一一对应:
- sdist:打包成
tar.gz
压缩包 - bdist_wheel:打包成
whl
2.4.3 调试
如果你期望确认一下打的包是否可用,你可以使用
python3 setup.py develop
来使我们的包JDistributer
生效。目的在于,pip
严格按照版本号校验升级,如果版本号一致则不会安装,倘若我们通过install
来校验,每次同版本号变更就需要先卸载,太麻烦了。
当一切验证完成,我们就可以考虑发布了。
2.5 发布
我们要去https://pypi.org/account/register/注册账号,后续上传包时需要使用。注意需要及时验证邮箱。
接下来我们需要生成API TOKEN
,好像从某一个时间点开始,不能直接通过账号密码登录了。
我们先访问Create API token · PyPI,创建一个Token,如果之前没创建项目的就先选择允许所有项目吧。复制好生成的Token
。
这个TOKEN每次都需要
请注意参照生成
Token
后页面展示的帮助,默认情况下这个Token
不会保存,每次上传都需要! 或者你可以修改$HOME/.pypirc
文件,将Token
写入
修改$HOME/.pypirc
,写入Token
:
[pypi]
username = __token__
password = pypi-[刚才复制的Token]
然后在刚才的JDistributer/python
目录,我们执行
twine upload dist/*
回车便开始上传。
wzq@WZQ-Laptop:~/JDistributer/python$ twine upload dist/*
Uploading distributions to https://upload.pypi.org/legacy/
Uploading JDistributer-0.0.2-py2.py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15.2/15.2 kB • 00:00 • ?
View at:
https://pypi.org/project/JDistributer/0.0.2/
然后我们就可以在JDistributer · PyPI上看到了。
2.6 省事做法
还记得咱们刚才写的setup.py
里的upload
方法么?
走完一边后,直接执行
python3 setup.py upload
直接打包、上传、打tag一条龙。