【Python从入门到精通】第029篇:Python 项目打包与发布 PyPI——从 pyproject.toml 到生产发布
上一篇【第028篇】自动化脚本实战——文件处理、定时任务与 Web 爬虫
下一篇【第030篇】Python 应用打包与部署——PyInstaller + Docker 实战
系列说明:本系列共 30 篇,旨在帮助Python学习者从零基础到精通。本系列强调实战导向,每篇文章都配有可运行的代码示例。本文为第 029 篇,聚焦于Python 项目打包与发布。
摘要
将自己的 Python 库发布到 PyPI,让全世界开发者都能通过pip install安装,是每位 Python 开发者的里程碑。本文从包格式(SDist vs Wheel)讲起,深入pyproject.toml(PEP 621)的完整配置,对比三大构建后端,演示 build + twine 发布工作流,并实战配置 GitHub Actions OIDC 可信发布,以及 SemVer 版本管理和 CHANGELOG 维护规范。
1. 发布包的基础概念
1.1 SDist vs Wheel
PyPI 上的包有两种格式:
| 格式 | 扩展名 | 说明 |
|---|---|---|
| Source Distribution(SDist) | .tar.gz | 源代码压缩包,安装时需编译 |
| Wheel | .whl | 预编译的二进制包,安装速度更快 |
对于纯 Python 包,Wheel 文件名格式为:{name}-{version}-py3-none-any.whl(py3兼容所有 Python 3,none无 C 扩展,any跨平台)
1.2 PyPI vs TestPyPI
| 环境 | 地址 | 用途 |
|---|---|---|
| TestPyPI | test.pypi.org | 测试发布流程,不影响真实用户 |
| PyPI | pypi.org | 正式发布,pip install默认来源 |
建议工作流:先发 TestPyPI 验证 → 确认无误 → 发布 PyPI。
2. 项目结构:src layout
推荐使用src布局,这是目前 Python 社区的最佳实践:
my-package/ ├── src/ │ └── mypackage/ │ ├── __init__.py │ ├── core.py │ └── utils.py ├── tests/ │ ├── conftest.py │ └── test_core.py ├── docs/ │ └── index.md ├── pyproject.toml ├── README.md ├── CHANGELOG.md └── LICENSE使用src布局的好处:
- 防止本地源码意外被 import(避免开发时忘记安装就能运行的假象)
- 与安装后的包路径一致,减少配置差异
- 更清晰地分离源码与项目文件
3. pyproject.toml 完整指南
pyproject.toml是 Python 包的统一配置文件(PEP 517/518/621 标准):
[build-system] requires = ["hatchling>=1.21"] build-backend = "hatchling.build" [project] name = "awesome-toolkit" # PyPI 包名(唯一) version = "0.2.1" # 遵循 SemVer description = "A toolkit for awesome things" readme = "README.md" license = { text = "MIT" } # SPDX 格式 requires-python = ">=3.11" keywords = ["toolkit", "automation", "cli"] authors = [ { name = "Your Name", email = "you@example.com" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Utilities", ] # 运行时依赖(最宽松兼容的版本范围) dependencies = [ "click>=8.1,<9.0", "rich>=13.0", "pydantic>=2.0", "httpx>=0.25", ] [project.optional-dependencies] # pip install awesome-toolkit[dev] dev = [ "pytest>=7.0", "pytest-cov", "ruff", "mypy", ] # pip install awesome-toolkit[docs] docs = [ "mkdocs-material", "mkdocstrings[python]", ] # pip install awesome-toolkit[all] all = [ "awesome-toolkit[dev,docs]", ] [project.urls] Homepage = "https://github.com/username/awesome-toolkit" Documentation = "https://awesome-toolkit.readthedocs.io" Repository = "https://github.com/username/awesome-toolkit" "Bug Tracker" = "https://github.com/username/awesome-toolkit/issues" Changelog = "https://github.com/username/awesome-toolkit/blob/main/CHANGELOG.md" [project.scripts] awesome = "awesome_toolkit.cli:main" [project.entry-points."awesome_toolkit.plugins"] default = "awesome_toolkit.plugins.default:DefaultPlugin"3.1 依赖版本说明符
click>=8.1,<9.0 # 兼容 8.1.x - 8.x.x,不包含 9.x pydantic~=2.0 # 兼容 2.0.0 - 2.x.x(等价于 >=2.0,<3.0) httpx>=0.25 # 大于等于 0.25,无上界(宽松约束) requests==2.31.0 # 精确版本(不推荐,过于严格)4. 三大构建后端对比
# 方案 1:Hatchling(推荐,功能丰富,原生支持 src layout) [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/mypackage"] # 方案 2:setuptools(历史最久,兼容性最好) [build-system] requires = ["setuptools>=68", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["src"] # 方案 3:PDM-backend(PDM 项目专用,支持 PEP 660) [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend"| 后端 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Hatchling | 配置简洁,功能强 | 相对年轻 | 新项目首选 |
| setuptools | 成熟稳定,生态最广 | 配置略繁琐 | 含 C 扩展 |
| PDM-backend | 与 PDM 工具链完美整合 | 依赖 PDM | PDM 管理的项目 |
5. 构建与发布工作流
5.1 安装工具
pipinstallbuild twine5.2 构建
# 在项目根目录执行python-mbuild输出:
dist/ ├── awesome_toolkit-0.2.1.tar.gz # SDist └── awesome_toolkit-0.2.1-py3-none-any.whl # Wheel5.3 发布前检查
# 检查包的元数据是否合规twine check dist/*# 输出示例Checking dist/awesome_toolkit-0.2.1-py3-none-any.whl: PASSED Checking dist/awesome_toolkit-0.2.1.tar.gz: PASSED5.4 发布到 TestPyPI
# 先发布到测试环境twine upload--repositorytestpypi dist/*# 从 TestPyPI 安装验证pipinstall--index-url https://test.pypi.org/simple/ awesome-toolkit5.5 发布到 PyPI
twine upload dist/*5.6 配置 API Token(.pypirc)
在 PyPI 账号设置中生成 API Token,然后配置~/.pypirc:
[distutils] index-servers = pypi testpypi [pypi] repository = https://upload.pypi.org/legacy/ username = __token__ password = pypi-AgEIcH...(你的 Token) [testpypi] repository = https://test.pypi.org/legacy/ username = __token__ password = pypi-AgEIcH...(TestPyPI Token)6. GitHub Actions 自动发布
6.1 OIDC 可信发布(Trusted Publishing)
这是目前最安全的 PyPI 发布方式,无需在 GitHub Secrets 存储 PyPI Token:
第一步:在 PyPI 配置可信发布
进入 PyPI → 你的项目 → Settings → Trusted Publishers → 添加:
- Owner:
your-username - Repository:
awesome-toolkit - Workflow:
publish.yml - Environment:
pypi(可选但推荐)
第二步:创建 GitHub Actions 工作流
# .github/workflows/publish.ymlname:Publish to PyPIon:release:types:[published]# 在 GitHub 创建 Release 时触发permissions:contents:readid-token:write# OIDC 认证必须jobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v4-name:Set up Pythonuses:actions/setup-python@v5with:python-version:'3.12'-name:Install build toolsrun:pip install build twine-name:Build packagerun:python-m build-name:Check packagerun:twine check dist/*-name:Upload artifactsuses:actions/upload-artifact@v4with:name:distpath:dist/publish:needs:buildruns-on:ubuntu-latestenvironment:name:pypiurl:https://pypi.org/project/awesome-toolkit/permissions:id-token:writesteps:-name:Download artifactsuses:actions/download-artifact@v4with:name:distpath:dist/-name:Publish to PyPIuses:pypa/gh-action-pypi-publish@release/v1# 使用 OIDC,无需 password 参数6.2 包含测试的完整 CI/CD
# .github/workflows/ci.ymlname:CIon:push:branches:[main]pull_request:branches:[main]jobs:test:runs-on:${{matrix.os}}strategy:matrix:os:[ubuntu-latest,windows-latest,macos-latest]python-version:['3.11','3.12']steps:-uses:actions/checkout@v4-name:Set up Python ${{matrix.python-version}}uses:actions/setup-python@v5with:python-version:${{matrix.python-version}}-name:Install dependenciesrun:|pip install -e ".[dev]"-name:Lintrun:ruff check src/ tests/-name:Type checkrun:mypy src/-name:Testrun:pytest tests/--cov=mypackage--cov-report=xml-v-name:Upload coverageuses:codecov/codecov-action@v4with:file:coverage.xml7. 版本管理:SemVer + PEP 440
7.1 语义化版本(SemVer)
MAJOR.MINOR.PATCH 1.0.0 → 1.0.1 # PATCH: bug 修复,向后兼容 1.0.1 → 1.1.0 # MINOR: 新功能,向后兼容 1.1.0 → 2.0.0 # MAJOR: 破坏性变更,不向后兼容7.2 PEP 440 版本规范
Python 包遵循 PEP 440,支持以下格式:
1.0.0 # 正式版本 1.0.0a1 # Alpha(内测) 1.0.0b2 # Beta(公测) 1.0.0rc1 # Release Candidate(候选发布) 1.0.0.post1 # 发布后修复(仅文档/元数据) 1.0.0.dev1 # 开发版本(不应发布到 PyPI)7.3 版本同步策略
将版本号定义在src/mypackage/__init__.py:
__version__='0.2.1'__all__=['__version__']在pyproject.toml中引用(使用 hatchling 动态版本):
[project] dynamic = ["version"] [tool.hatch.version] path = "src/mypackage/__init__.py"这样只需修改一处,构建时自动同步。
8. CHANGELOG 维护
遵循 Keep a Changelog 规范:
# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - 新功能描述 ## [0.2.1] - 2024-11-15 ### Fixed - 修复了在 Windows 上路径解析错误 (#45) - 修复了 `--timeout` 参数类型错误 ## [0.2.0] - 2024-10-20 ### Added - 新增 `--format` 参数支持 JSON/YAML/CSV 输出 (#38) - 新增 Python 3.12 支持 ### Changed - `process()` 函数签名变更(BREAKING),新增必填参数 `encoding` ### Deprecated - `old_api()` 已废弃,将在 1.0.0 中移除 ### Removed - 移除对 Python 3.9 的支持 ## [0.1.0] - 2024-09-01 ### Added - 初始发布 [Unreleased]: https://github.com/username/awesome-toolkit/compare/v0.2.1...HEAD [0.2.1]: https://github.com/username/awesome-toolkit/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/username/awesome-toolkit/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/username/awesome-toolkit/releases/tag/v0.1.09. 开源许可证选择
| 许可证 | 允许商业使用 | 要求开源 | 专利授权 | 适用场景 |
|---|---|---|---|---|
| MIT | ✓ | ✗ | ✗ | 宽松,适合大多数库 |
| Apache 2.0 | ✓ | ✗ | ✓ | 需专利保护的企业项目 |
| GPL v3 | ✓ | ✓(同许可证) | ✓ | 希望衍生品也开源 |
| BSD 3-Clause | ✓ | ✗ | ✗ | 类似 MIT,略严格 |
| LGPL | ✓ | 仅库本身 | ✗ | 库可被商业软件使用 |
在pyproject.toml中用 SPDX 标识符:
license = { text = "MIT" } # 或 license = { file = "LICENSE" } classifiers = [ "License :: OSI Approved :: MIT License", ]10. 完整发布检查清单
# 1. 更新版本号(src/mypackage/__init__.py 或 pyproject.toml)# 2. 更新 CHANGELOG.md(将 [Unreleased] 改为新版本号)# 3. 运行测试pytest tests/-v# 4. 代码检查ruff check src/ mypy src/# 5. 构建python-mbuild# 6. 检查构建产物twine check dist/* pipinstalldist/*.whl --dry-run# 可选:本地安装验证# 7. 发布 TestPyPItwine upload--repositorytestpypi dist/* pipinstall--index-url https://test.pypi.org/simple/ awesome-toolkit==0.2.1# 8. 确认无误后发布 PyPItwine upload dist/*# 9. 在 GitHub 创建 Release Taggittag v0.2.1gitpush origin v0.2.1# 在 GitHub 网页创建 Release11. 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 包名已被占用 | PyPI 名称不可重复 | 检查 pypi.org,换一个名字 |
twine check报 README 格式错误 | Markdown 语法问题 | 检查 README.md,安装readme-renderer[md] |
上传报403 Forbidden | Token 无效或无权限 | 重新生成 PyPI Token |
| 旧版本无法覆盖 | PyPI 不允许重新上传相同版本 | 更新版本号后重新发布 |
pip install安装后 import 失败 | 包名与模块名不一致 | 检查pyproject.toml的packages.find.where |
| Windows 安装时编码错误 | .pypirc文件编码 | 使用 UTF-8 无 BOM 格式保存 |
12. 小结
| 知识点 | 核心要点 |
|---|---|
| 包格式 | SDist(源码包)vs Wheel(预编译包) |
| src 布局 | src/mypackage/隔离源码与项目文件 |
| pyproject.toml | PEP 621 元数据 +[build-system]+[project] |
| 依赖版本 | >=x.y,<x+1(兼容约束)、~=x.y(兼容发布) |
| 构建后端 | Hatchling(推荐)/ setuptools / PDM-backend |
| 发布工具 | python -m build+twine check+twine upload |
| OIDC 发布 | GitHub Actions + PyPI Trusted Publishers,无 Token |
| SemVer | MAJOR.MINOR.PATCH,破坏性变更升 MAJOR |
| PEP 440 | alpha/beta/rc/post 版本后缀 |
| CHANGELOG | Keep a Changelog 格式 + 版本对比链接 |
| 许可证 | MIT(宽松)/ Apache 2.0(含专利)/ GPL(强 copyleft) |
上一篇【第028篇】自动化脚本实战——文件处理、定时任务与 Web 爬虫
下一篇【第030篇】Python 应用打包与部署——PyInstaller + Docker 实战
参考资料
- Python Packaging User Guide
- PEP 621 — Storing project metadata in pyproject.toml
- Hatchling 文档
- PyPA GitHub Action for PyPI Publishing
- Keep a Changelog
- Semantic Versioning 2.0.0
- Choose a License
