PyPI 一键发布助手
把 Python 项目打包发布到 PyPI,全流程校验、构建、上传一条龙。
使用场景
- "把这个包发布到 PyPI"
- "上传到 pypi" / "发包" / "publish to pypi"
- "上架 PyPI" / "让别人 pip install"
前置依赖
需要已安装 build 和 twine:
pip install build twine
工作流程
Step 0 — 信息收集
- 向用户确认:
- 包名(pyproject.toml 中的[project].name)
- 版本号(语义化版本 X.Y.Z,参考 https://semver.org)
- PyPI API Token(pypi-开头的长字符串)
- 是否已在 PyPI 注册账号 - 如果用户没提供 token,引导去 https://pypi.org/manage/account/token/ 生成
- Scope 选 "Entire account"(首次发布新项目时必须)
- 发布成功后建议用户把 scope 收紧到项目级别
Step 1 — 校验 pyproject.toml(⚠️ 关键字段检查清单)
检查清单:
| 字段 | 必填 | 说明 |
|---|---|---|
[project].name |
✅ | 包名,全小写连字符分隔(如 mysql-mcp-pro) |
[project].version |
✅ | 版本号,与要发布的 tag 一致 |
[project].description |
✅ | 一句话介绍,显示在 PyPI 搜索结果 |
[project].readme |
✅ | {file = "README.md", content-type = "text/markdown"} |
[project].requires-python |
✅ | >=3.10 等 |
[project].license |
✅ | {text = "MIT"} 或 {file = "LICENSE"} |
[project].authors |
✅ | [{name = "..."}] |
[project].keywords |
推荐 | 搜索关键词列表 |
[project].classifiers |
推荐 | 开发状态、受众、Python 版本、License、Topic |
[project].dependencies |
✅ | 运行时依赖,带版本下限 |
[project].urls |
推荐 | Homepage / Repository / Issues / Changelog |
[project.scripts] |
可选 | CLI 入口,cmd-name = "module:main" |
[build-system] |
✅ | requires = ["hatchling>=..."], build-backend = "hatchling.build" |
常见错误:
- ❌ readme = "README.md" 缺 content-type → PyPI 页面 markdown 不渲染
- ❌ 缺 license → 包列表不显示 license badge
- ❌ wheel 的 packages = ["."] 会把整个目录打包进去 → 应用 only-include = ["my_module.py"] 或正确的包路径
- ❌ 版本号带 v 前缀(v2.0.0),PyPI 版本必须是纯数字 2.0.0
Step 2 — 检查包名可用
import urllib.request, json
try:
urllib.request.urlopen(f"https://pypi.org/pypi/{pkg_name}/json", timeout=5)
print("已存在!")
except urllib.error.HTTPError as e:
if e.code == 404:
print("可用 ✓")
如果包名已被占用,提示用户改名(换个更独特的名字)。
Step 3 — 清理 + 构建
# 清理旧产物
rm -rf dist build *.egg-info
# 构建 wheel 和 sdist
python -m build
注意:
- 首次构建会创建隔离 venv 并安装 build-system requires,耐心等
- 成功后应输出 Successfully built xxx-X.Y.Z.tar.gz and xxx-X.Y.Z-py3-none-any.whl
Step 4 — 校验产物(twine check)
twine check dist/*
必须全部 PASSED,有错误必须修复后重新构建:
- The project's long_description has invalid markup → README.md 语法错误
- Missing meta-data: ... → pyproject.toml 缺字段
Step 5 — 检查 wheel 内容
python -c "
import zipfile, glob
whl = glob.glob('dist/*.whl')[0]
print('Wheel 包含文件:')
for n in zipfile.ZipFile(whl).namelist():
print(' ', n)
"
# 同时检查 sdist(tar.gz)内容,防止 scripts/、测试数据、敏感文件被打包
python -c "
import tarfile, glob
sd = glob.glob('dist/*.tar.gz')[0]
with tarfile.open(sd) as t:
print('sdist 包含文件:')
for m in t:
if m.isfile():
print(' ', m.name)
"
sdist 关键配置(hatchling):
[tool.hatch.build.targets.sdist] 一定要用 include 白名单,默认会把所有 .py 文件打包,包括 scripts/、tests/、本地诊断脚本:
[tool.hatch.build.targets.sdist]
include = [
"/server.py", # 主模块
"/README.md",
"/LICENSE",
"/.env.example",
"/pyproject.toml",
# 不要列 scripts/!不要列 tests/!
]
确认:
- 主模块/包文件在里面(server.py / mypackage/)
- *.dist-info/METADATA 存在
- *.dist-info/entry_points.txt 存在(如果定义了 CLI)
- LICENSE 被包含在 *.dist-info/licenses/
- 没有 .env、__pycache__、.git、scripts/(本地工具)、测试数据、含硬编码密码的文件
- 没有 任何含实际密码/token 的字符串(grep 搜索):
bash
python -c "
import tarfile, glob
with tarfile.open(glob.glob('dist/*.tar.gz')[0]) as t:
for m in t:
if m.isfile():
data = t.extractfile(m).read().decode('utf-8', errors='replace')
for needle in ['your_password','sk-','pypi-Ag','hardcoded_secret']:
if needle in data:
print(f'❌ {m.name} contains {needle}!')
"
Step 6 — 上传到 PyPI(⚠️ Token 安全处理)
绝对不要把 token 写进命令行参数(会被 shell history 记录)或提交到代码。
正确方式:通过环境变量传:
TWINE_USERNAME=__token__ \
TWINE_PASSWORD='pypi-YourActualTokenHere' \
twine upload --non-interactive dist/*
关键点:
- 用户名固定 __token__(两个下划线)
- --non-interactive 避免卡住等待输入
- 上传完成后不要把 token 写到任何文件里
- 提醒用户:token 已使用完毕,可到 PyPI 后台删除或缩 scope
成功输出形如:
View at:
https://pypi.org/project/<pkg-name>/<version>/
Step 7 — 验证安装
上传成功后等 1-5 分钟(PyPI CDN 同步),验证:
# 新 venv 或临时目录
pip install --no-cache-dir <pkg-name>==<version>
# CLI 入口可用测试
<pkg-name> --help
Step 8 — 同步 Git tag + push
# 更新 pyproject.toml 中的 version(如果之前是占位符)
git add pyproject.toml
git commit -m "chore: release vX.Y.Z"
git tag -a vX.Y.Z -m "vX.Y.Z
<changelog 要点>"
git push origin main
git push origin vX.Y.Z
如果项目在 GitHub,自动发布 Release:
gh release create vX.Y.Z --title "vX.Y.Z" --notes "..."
一键命令模板
# 完整流程(假设 pyproject.toml 已配置好,token 存到环境变量)
pip install build twine
rm -rf dist build *.egg-info
python -m build
twine check dist/*
TWINE_USERNAME=__token__ TWINE_PASSWORD="$PYPI_TOKEN" twine upload --non-interactive dist/*
git tag -a v$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") -m "release"
git push origin main --tags
TestPyPI(强烈建议先测)
首次发布或重大版本前,建议先用 TestPyPI 验证:
# 1. 去 https://test.pypi.org/account/register/ 注册
# 2. 生成 TestPyPI API token(test.pypi.org/manage/account/token/)
# 3. 上传到 test
TWINE_USERNAME=__token__ TWINE_PASSWORD='pypi-TestToken...' \
twine upload --repository testpypi --non-interactive dist/*
# 4. 从 test 安装验证
pip install --index-url https://test.pypi.org/simple/ <pkg-name>
TestPyPI 验证通过再上传到正式 PyPI,避免污染正式包索引。
常见坑
- 包名冲突:PyPI 包名全局唯一,被占了只能改名
- 版本号不能重传:同一个版本号不能上传两次,哪怕删了也不行
- 如果上传后发现 metadata 要改(比如 description 有敏感词),必须升版本号(如 2.0.0 → 2.0.1)
- 旧版本在 PyPI 上无法删除(只能 yank),pip install默认装最新版,但老用户可能装到旧版 - README 中文乱码:确保文件 UTF-8 编码,content-type 正确
- wheel 不包含模块:
[tool.hatch.build.targets.wheel]配置错了 - sdist 默认包含所有源码:不加 include 白名单会把 scripts/ 测试/诊断脚本一起打包
- sdist 缺文件:
[tool.hatch.build.targets.sdist].include没列全 - Token 权限不足:新包首次发布必须用 account-scoped token;项目已存在后可用 project-scoped token
- 依赖版本过低:
requires-python = ">=3.10"但代码用了 3.11 语法 → 安装后运行报错 - 包元数据里别写敏感信息:
description字段里不要出现"hardcoded password"/"leaked"/"security fix"等字样,保持中性("improve configuration"/"bug fixes") - twine upload 不要把 token 写命令行:会进入 shell history;用环境变量或 .pypirc
关联 skill
github-upload— PyPI 发布前先确保代码在 GitHub 上(方便 source distribution 溯源)