如何将Python包发布到PyPI

引言

我最近开发并发布了我的第一个 Python 库到 PyPI。本文并不是关于库本身的技术细节,而是记录了从一无所知到发布它的过程

已发布的库:

我希望这对任何想要创建库但不知道从何开始的人有所帮助。


原因

在使用 FastAPI 开发实时通信应用时,我遇到了 WebSocket 连接不稳定的问题。

具体来说:

  • 意外的连接中断
  • 没有心跳机制
  • 僵尸连接的积累
  • 复杂的重连处理
  • 繁琐的优雅关闭实现

我不断编写类似的代码来解决这些问题,当我查看 GitHub Issues 和 Stack Overflow 时,发现许多开发者面临着相同的挑战。

“那么,也许将其作为一个通用库发布是有价值的。”

这时我开始了开发。


项目结构

第一个挑战是决定项目结构。经过研究,我采用了这个标准结构:

project-root/
├── src/
│   └── fastapi_websocket_stabilizer/
│       ├── __init__.py
│       ├── manager.py
│       ├── config.py
│       └── ...
├── tests/
├── README.md
├── LICENSE
└── pyproject.toml

关键点

  • 使用 src 布局(在测试期间使用已安装的包)
  • 包名使用连字符,模块名使用下划线
  • 选择 MIT 许可证(以便广泛采用)

pyproject.toml 配置

现代 Python 项目使用 pyproject.toml 代替 setup.py 进行配置。以下是我使用的配置:

[build-system]
requires = ["setuptools>=70.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "fastapi-websocket-stabilizer"
version = "0.1.0"
description = "一个为 FastAPI 应用程序提供生产就绪的 WebSocket 稳定层"

readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
    {name = "FastAPI WebSocket 稳定器贡献者"}
]
keywords = [
    "fastapi",
    "websocket",
    "连接管理",
    "心跳",
    "优雅关闭",
]
classifiers = [
    "开发状态 :: 4 - 测试版",
    "环境 :: Web 环境",
    "目标受众 :: 开发者",
    "操作系统 :: 操作系统无关",

以下是您提供的英文技术文章内容的中文翻译:

"编程语言 :: Python",
"编程语言 :: Python :: 3",
"编程语言 :: Python :: 3.10",
"编程语言 :: Python :: 3.11",
"编程语言 :: Python :: 3.12",
"主题 :: 互联网 :: WWW/HTTP",
"主题 :: 软件开发 :: 库 :: Python 模块",
]

依赖项 = [
    "fastapi>=0.95.0",
]

[项目.可选依赖项]
开发 = [
    "pytest>=7.0",
    "pytest-asyncio>=0.21.0",

    "pytest-cov>=4.0",
    "black>=23.0",
    "ruff>=0.1.0",
    "mypy>=1.0",
    "uvicorn>=0.20.0",
]

[project.urls]
主页 = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer"
仓库 = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer"
文档 = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer#readme"
问题 = "https://github.com/yuuichieguchi/fastapi-websocket-stabilizer/issues"
[tool.setuptools]
packages = ["fastapi_websocket_stabilizer"]
package-dir = {"" = "src"}

[tool.black]
line-length = 100
target-version = ["py310", "py311", "py312"]

[tool.ruff]
line-length = 100
target-version = "py310"
select = ["E", "F", "W", "I"]

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_untyped_calls = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

要点:

  • classifiers 指定 PyPI 上的分类
  • optional-dependencies 分离开发工具
  • tool.* 部分集成了 linter、formatter 和测试配置

设置 PyPI 账户

创建账户

  • https://pypi.org 创建一个账户
  • 验证您的电子邮件地址
  • 启用双因素身份验证(推荐)

生成 API 令牌

出于安全考虑,请使用 API 令牌而不是密码。

  • 登录 PyPI
  • 转到账户设置 → API 令牌
  • 点击“添加 API 令牌”
  • 选择范围:“整个账户”(用于初始发布)
  • 安全存储生成的令牌

重要: 令牌只显示一次,请确保保存。

配置身份验证

创建一个 ~/.pypirc 文件:

[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc... (你的令牌)

构建包

安装所需工具

pip install --upgrade build twine setuptools wheel
### 构建

python -m build

成功后,将在 dist/ 目录中生成以下文件:

  • .whl 文件(wheel 格式)
  • .tar.gz 文件(源代码分发格式)

故障排除:InvalidDistribution 错误

在我第一次构建时,遇到了这个错误:

InvalidDistribution: 未识别或格式错误的字段 'license-file'

原因

这是由于 twinepackaging 库之间的版本冲突造成的。

解决方案

# 更新相关工具
pip install --upgrade packaging build setuptools wheel twine

# 清除缓存
rm -rf dist build *.egg-info

# 重新构建
python -m build

这解决了问题。


上传到 PyPI

使用 TestPyPI 进行测试(推荐)

在上传到生产环境的 PyPI 之前,我建议先在 TestPyPI 上进行测试:

python -m twine upload --repository testpypi dist/*

测试PyPI: https://test.pypi.org

上传到生产环境

如果一切看起来都很好,请上传到生产环境:

python -m twine upload dist/*
上传完成后,您的包将在几分钟内可以通过 https://pypi.org/project/your-package-name/ 访问。

验证

pip install fastapi-websocket-stabilizer

现在世界上任何人都可以使用此命令进行安装。


发布后的维护

版本管理

遵循语义版本控制:

  • 重大: 破坏性变更
  • 次要: 向后兼容的功能添加
  • 修补: 向后兼容的错误修复

示例: 0.1.00.1.1 (错误修复) → 0.2.0 (新功能) → 1.0.0 (稳定版)

更新过程

  1. 修改代码
  2. pyproject.toml 中更新版本
  3. 更新 CHANGELOG.md
  4. 重新构建并重新上传
python -m build
python -m twine upload dist/*
文档

为了用户,我准备了:

  • 在 README.md 中的使用示例和 API 参考
  • 用于 IDE 补全的类型提示
  • 通过文档字符串提供的函数描述

反思

技术学习

通过库的开发,我学到了:

  • Python 打包机制
  • 类型提示的重要性
  • 维护测试覆盖率
  • 设置 CI/CD

给那些创建他们第一个库的人

如果你在想“我想创建一个库,但这似乎很艰巨”,以下是我的建议:

  • 从小开始:你不需要一开始就构建一个庞大的项目
  • 解决真实问题:解决你所面临的问题的库是有价值的
  • 在 TestPyPI 上练习:你可以在上线之前在一个预发布环境中进行测试
  • 编写良好的文档:清晰的使用说明能吸引用户
  • 欢迎反馈:问题是改进的机会

技术障碍比你想象的要低。更大的障碍可能是“发布的勇气”。


结论

发布我的第一个库是一次宝贵的经历,不仅在技术学习上有所收获,也让我参与到了开源软件(OSS)社区中。

如果你在想“我想试着做一个”,我鼓励你迈出第一步。

参考文献:


更多