本文面向有一定 Python 基础、希望将代码规范化为可安装包并发布到 PyPI 的工程师。你将学会:

  • 如何创建标准的 Python 包工程骨架(src 布局)
  • pyproject.toml 中使用 PEP 621 声明元数据与 project.scripts 生成命令行脚本
  • 使用 build 本地构建分发产物(sdist/wheel)
  • 使用 twine 校验并上传到 TestPyPI 与 PyPI
  • 常见问题与排错要点

参考标准:PEP 517/518(构建系统),PEP 621(项目元数据)。

适用环境

  • Python ≥ 3.8(推荐 3.10+)
  • macOS/Linux/Windows
  • 包管理:pippipx

一、项目骨架(src 布局)

推荐使用「src 布局」以避免导入歧义,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
mycli/
├─ pyproject.toml
├─ README.md
├─ LICENSE
├─ src/
│ └─ mycli/
│ ├─ __init__.py
│ ├─ __main__.py
│ └─ cli.py
└─ tests/
└─ test_basic.py
  • src/mycli:包代码根目录
  • cli.py:命令行入口逻辑(将通过 project.scripts 暴露为命令)
  • __main__.py:支持 python -m mycli 直接运行
  • pyproject.toml:声明构建系统与项目元数据(PEP 621)

二、编写包代码(示例:命令行工具)

所有代码与注释统一使用英文(便于社区协作与审核)。

src/mycli/cli.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import argparse


def main() -> None:
"""Entry point for the mycli command line tool."""
parser = argparse.ArgumentParser(
prog="mycli",
description="A tiny example CLI that greets the user."
)
parser.add_argument(
"-n", "--name",
default="World",
help="Name to greet"
)
args = parser.parse_args()
print(f"Hello, {args.name}!")

src/mycli/__init__.py

1
2
__all__ = ["__version__"]
__version__ = "0.1.0"

src/mycli/__main__.py

1
2
3
4
from .cli import main

if __name__ == "__main__":
main()

三、pyproject.toml(PEP 621 + project.scripts)

示例使用 setuptools 作为构建后端,启用 PEP 621 元数据与 project.scripts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "mycli-example"
version = "0.1.0"
description = "A tiny example CLI published to PyPI."
readme = "README.md"
requires-python = ">=3.8"
license = { text = "MIT" }
authors = [{ name = "Your Name", email = "you@example.com" }]
keywords = ["cli", "example", "greeting"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = []

# This creates an executable 'mycli' in the user's PATH after installation.
[project.scripts]
mycli = "mycli.cli:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

要点:

  • project.scriptsmycli 命令映射到 mycli.cli:main
  • 采用 src 布局需设置 package-dirpackages.find.where
  • 设置 requires-pythonclassifiers 便于用户检索与兼容性声明

四、README 与 LICENSE

  • README.md 作为包主页与长描述,需简要说明安装与用法
  • 许可证建议选择常见的开源协议(如 MIT/Apache-2.0)

README.md 示例:

1
2
3
4
5
6
7
8
# mycli-example

Simple CLI that greets you.

## Install

```bash
pip install mycli-example

Usage

1
mycli --name Alice
1
2
3
4
5
6
7
8
9
10
11
12
13
14

---

## 五、安装工具并本地构建

推荐使用 `pipx` 安装构建与发布工具,避免污染项目虚拟环境:

```bash
# Install tools
pipx install build
pipx install twine

# Or use pip (in a temporary/clean venv)
python -m pip install --upgrade build twine

执行构建:

1
2
python -m build
# 输出产物位于 dist/ 目录,包含 .whl 与 .tar.gz

构建检查:

1
twine check dist/*

六、发布到 TestPyPI 与 PyPI

强烈建议先在 TestPyPI 验证再发布至 PyPI。

方式 A:使用 .pypirc 管理凭据(推荐)

创建 ~/.pypirc(macOS/Linux):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[distutils]
index-servers =
pypi
testpypi

[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = pypi-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

上传到 TestPyPI:

1
twine upload -r testpypi dist/*

验证安装(注意使用 TestPyPI 索引):

1
2
3
4
python -m pip install --index-url https://test.pypi.org/simple --extra-index-url https://pypi.org/simple mycli-example

# 验证命令是否可用
mycli --name Bob

确认无误后,上传至 PyPI:

1
twine upload -r pypi dist/*

方式 B:直接指定仓库 URL(无需 .pypirc)

1
2
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
twine upload --repository-url https://upload.pypi.org/legacy/ dist/*

凭据将通过交互提示或环境变量读取(CI 中建议使用环境变量/密文)。


七、使用与验证

安装后应具备两种使用方式:

1
2
3
4
5
# 1) 通过脚本(来自 project.scripts)
mycli --name Carol

# 2) 通过模块运行(__main__.py)
python -m mycli --name Carol

八、常见问题与排错

  • File already exists(重复版本):PyPI 不允许覆盖同版本;升级 version(如 0.1.1)并重新构建上传
  • Invalid long_descriptiontwine check 失败,多为 README 渲染问题;将 readme = "README.md" 并确保 Markdown 合规
  • Script not found:检查 project.scripts 的目标路径是否正确(mycli.cli:main)且包已包含在构建中(packages.find 设置)
  • ImportError after install:多因未使用 src 布局或测试时在项目根运行导致导入到源码;建议在全新 venv 中验证
  • 401/403 授权失败:确认使用 __token__ 用户名与有效 PyPI Token;避免泄露令牌(使用 CI 密文)
  • Wheel 缺少文件:确认未被 MANIFEST.in/忽略规则误排除;或切换到现代 PEP 621 配置并避免遗留 setup.cfg 混用

九、在 CI 中自动发布(可选思路)

  • 在 GitHub Actions/其他 CI 中使用发布工作流
  • 构建步骤同本地:python -m buildtwine checktwine upload
  • 凭据使用仓库密文(如 PYPI_API_TOKEN),并在 CI 步骤设置为环境变量

官方参考:


十、总结

  • 使用 PEP 621 在 pyproject.toml 声明元数据,结合 project.scripts 生成命令行
  • build 统一构建 sdist/wheel,twine 校验与上传
  • 先 TestPyPI 验证,再发布至 PyPI
  • 采用 src 布局、明确 Python 版本与分类器,提升可维护性与可发现性

本文由 AI 辅助生成,如有错误或建议,欢迎指出。