打包与发布
Python打包与发布详解:setuptools、pyproject.toml。
前置知识
- 弱引用:建议先完成前一篇的学习
学习目标
- 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
- 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「3. 理论推导与原理解析」的核心机制、典型用法与常见陷阱
- 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
- 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱
1. 历史动机与发展脉络
理解 Python 打包生态的现状,必须穿越 30 余年的演进史。这条路径上有大量被废弃的方案与教训,对今天的工程决策仍然具有现实指导意义。
1.1 上古时代:distutils(1994-2008)
Python 1.6 引入的 distutils 是官方第一代打包工具,由 Greg Ward 在 1994 年设计。其配置文件 setup.py 通过命令式脚本描述元数据:
# setup.py(distutils 时代,Python 1.6-2.x)
from distutils.core import setup
setup(
name="mypackage",
version="1.0",
py_modules=["mymodule"],
)
distutils 的核心问题:
- 元数据与逻辑耦合在 Python 脚本中,无法被静态工具读取;
- 不支持依赖声明(
install_requires); - 扩展 C 模块的接口僵化;
- 没有可插拔的构建后端机制。
1.2 setuptools 时代:Python 2.x 主导(2004-2018)
Phillip J. Eby 在 2004 年创建 setuptools,作为 distutils 的超集:
- 引入
install_requires、extras_require依赖声明; - 引入
entry_points插件机制(被 Flask、Pytest、Celery 大量使用); - 引入
pkg_resources运行时包发现; - 2008 年
pip出现后,setuptools 成为事实标准。
但 setuptools 也带来了新的痛点:
setup.py仍然是命令式 Python 脚本,安装时必须执行任意代码(安全风险);easy_install安装器不支持依赖解析;egg二进制格式被社区分裂(wheel取而代之);- 配置散落在
setup.py、setup.cfg、MANIFEST.in三个文件中。
1.3 PEP 517/518/621:声明式时代(2015-2022)
社区意识到 setup.py 必须执行任意代码这一根本缺陷,提出了一系列 PEP:
- PEP 518(2016)引入
pyproject.toml作为项目配置根文件,定义[build-system]表指定构建依赖; - PEP 517(2017)定义构建后端接口(
build_wheel、build_sdist、prepare_metadata_for_build_wheel),允许 setuptools 之外的构建后端; - PEP 621(2020)标准化
[project]表,将元数据从构建后端专属语法中解耦; - PEP 660(2021)标准化可编辑安装(
pip install -e)的 wheel 实现; - PEP 440(2014,多次更新)定义 Python 版本号规范,被 setuptools、hatch、poetry 共同遵守。
1.4 现代构建后端(2022 至今)
| 构建后端 | 首次发布 | 特点 | 适用场景 |
|---|---|---|---|
| setuptools | 2004 | 历史包袱重但生态最完整 | 兼容遗留项目、C 扩展 |
| hatchling | 2022 | PEP 621 原生支持,配置极简 | 新项目首选 |
| flit | 2016 | 极简,仅支持纯 Python 包 | 工具类小项目 |
| poetry-core | 2020 | 自带依赖解析与锁定 | 应用项目 |
| pdm-backend | 2022 | PEP 621 标准,配合 pdm 工具 | PEP 582 爱好者 |
| maturin | 2019 | Rust 扩展专用 | PyO3 项目 |
| scikit-build-core | 2022 | CMake 后端 | 复杂 C/C++/CUDA 扩展 |
1.5 发布基础设施演进
2003 PyPI 上线(CPython 仓库)
↓
2013 PyPI Warehouse 重写计划启动
↓
2018 Warehouse 上线,废弃旧 PyPI
↓
2022 PyPI 强制开启 2FA(API Token)
↓
2023 Trusted Publisher(OIDC)成为推荐方案
↓
2024 要求所有上传包必须经过 CI 校验
2. 形式化定义
2.1 PEP 517 构建后端接口
PEP 517 定义了构建后端(build backend)的三个核心钩子(hooks)。设 为构建后端模块, 为源码目录, 为输出 wheel 路径,则接口形式化为:
其中:
- 是 wheel 输出目录;
- 是来自
[tool.{backend}]表的配置字典; - 是可选的元数据缓存目录(用于避免重复构建);
- 返回值是 wheel 文件名(不含路径)。
2.2 Wheel 文件名规范(PEP 427)
wheel 文件名遵循以下 BNF 文法:
举例:
numpy-1.26.4-cp312-cp312-win_amd64.whlcp312Python tag:CPython 3.12cp312ABI tag:CPython 3.12 ABIwin_amd64platform:Windows x86_64
requests-2.32.3-py3-none-any.whlpy3Python tag:任何 Python 3noneABI tag:无 ABI 限制(纯 Python)anyplatform tag:任意平台
2.3 PEP 440 版本号形式化
PEP 440 定义了 Python 版本号的严格文法:
其中:
- (极少使用,仅在版本号重置时);
- (如
1.2.3); - (如
a1、b2、rc3); - (如
1.0.post1); - (如
1.0.dev4); - (如
+local.1)。
版本排序严格遵循:
2.4 依赖说明符(PEP 508)
PEP 508 定义了依赖说明符的完整文法:
举例:
requests >= 2.28.0, < 3.0tomli >= 2.0 ; python_version < "3.11"numpy[full] >= 1.26 ; platform_python_implementation != "PyPy"
环境标记(marker)的关键变量:
| 变量 | 含义 | 示例取值 |
|---|---|---|
python_version | Python 版本 | "3.12" |
python_full_version | 完整 Python 版本 | "3.12.4" |
os_name | 操作系统 | "posix"、"nt" |
sys_platform | 系统平台 | "linux"、"win32"、"darwin" |
platform_machine | CPU 架构 | "x86_64"、"arm64" |
platform_python_implementation | 解释器实现 | "CPython"、"PyPy" |
implementation_version | 解释器版本 | "3.12.4" |
3. 理论推导与原理解析
3.1 sdist 与 wheel 的本质差异
sdist(source distribution):源码包,是一个 .tar.gz 文件,包含 pyproject.toml、源码、PKG-INFO。安装时需要执行构建后端的 build_wheel 钩子,将源码编译为 wheel 后再安装。
wheel(built distribution):预编译包,是一个 .zip 文件(扩展名 .whl),包含可直接复制到 site-packages 的文件结构。安装时无需执行任何构建逻辑,性能与安全性均更优。
形式化地,设 为从 sdist 安装的总耗时:
而从 wheel 安装:
由于 (尤其涉及 C 扩展编译时),wheel 显著优于 sdist。
3.2 src 布局的理论优势
设项目结构存在两种布局:
flat 布局:
flowchart TD
T0["project/"]
T1["mypackage/"]
T2["__init__.py"]
T3["pyproject.toml"]
T0 --> T1
T2 --> T3
src 布局:
flowchart TD
T0["project/"]
T1["src/"]
T2["mypackage/"]
T3["__init__.py"]
T4["pyproject.toml"]
T0 --> T1
T3 --> T4
在 flat 布局下,当用户在项目根目录运行 python -c "import mypackage" 时,会从当前目录导入未安装的源码,导致:
- 测试时测的不是已安装包,而是源码;
importlib.metadata.version("mypackage")失败(因为没有安装元数据);- 路径相关问题(
__file__指向源码而非site-packages)。
src 布局强制要求先安装再测试:
这保证了测试覆盖的是真实安装产物,符合 MIT 软件工程课程的”测你所发”原则。
3.3 可编辑安装(PEP 660)的原理
PEP 660 之前,可编辑安装通过 easy-install.pth 文件实现,将项目根目录加入 sys.path,存在以下问题:
- flat 布局下的
import歧义未解决; - 包元数据不可用;
- 多个项目共用同一
sys.path容易冲突。
PEP 660 要求构建后端实现 build_editable 钩子,生成一个特殊的 wheel:
- 在
site-packages中放置一个.pth文件或自定义导入钩子; - 同时生成元数据目录,使
importlib.metadata可正常工作; - 支持 src 布局、自定义导入路径。
3.4 Trusted Publisher(OIDC)的安全模型
传统 PyPI 发布依赖 API Token,存在以下风险:
- Token 泄漏后可被任意人上传恶意版本;
- Token 必须存储在 CI Secrets 中,跨组织协作困难;
- 无法区分发布者身份。
Trusted Publisher 基于 OpenID Connect(OIDC)协议:
- GitHub Actions 通过
id-token: write权限向 PyPI 提交 OIDC token; - PyPI 验证 token 签名、签发者(GitHub)、仓库、workflow 文件路径;
- 验证通过后允许一次性发布当前版本。
形式化地,发布权限 满足:
任何一项不符则拒绝。这种方案下,攻击者即使窃取 CI 配置也无法在其他仓库重放 token。
4. 代码示例
4.1 最小可发布项目
完整目录结构:
flowchart TD
T0["minimal-package/"]
T1["pyproject.toml"]
T2["README.md"]
T3["LICENSE"]
T4["src/"]
T5["minimal/"]
T6["__init__.py"]
T7["cli.py"]
T8["py.typed"]
T9["tests/"]
T10["__init__.py"]
T11["test_core.py"]
T0 --> T1
T0 --> T2
T0 --> T3
T0 --> T4
T8 --> T9
T9 --> T10
T9 --> T11
pyproject.toml:
# 构建系统配置(PEP 518)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# 项目元数据(PEP 621)
[project]
name = "minimal"
version = "0.1.0"
description = "一个最小可发布的 Python 包示例"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Your Name", email = "you@example.com"},
]
keywords = ["example", "tutorial"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = []
# 可选依赖(PEP 621 optional-dependencies)
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"mypy>=1.10",
"ruff>=0.5",
]
# 命令行入口点
[project.scripts]
minimal = "minimal.cli:main"
# 项目 URL(PEP 621)
[project.urls]
Homepage = "https://github.com/you/minimal"
Repository = "https://github.com/you/minimal"
Issues = "https://github.com/you/minimal/issues"
Changelog = "https://github.com/you/minimal/blob/main/CHANGELOG.md"
# hatchling 特定配置
[tool.hatch.build.targets.wheel]
packages = ["src/minimal"]
src/minimal/__init__.py:
"""Minimal 包:示例项目结构。"""
from __future__ import annotations
__version__ = "0.1.0"
__all__ = ["__version__"]
src/minimal/cli.py:
"""命令行入口模块。"""
from __future__ import annotations
import argparse
import sys
from typing import Sequence
def main(argv: Sequence[str] | None = None) -> int:
"""主入口函数。
:param argv: 命令行参数,None 时使用 sys.argv
:return: 退出码
"""
parser = argparse.ArgumentParser(
prog="minimal",
description="Minimal 包命令行工具",
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}" if (__version__ := _get_version()) else "%(prog)s",
)
parser.add_argument(
"name",
nargs="?",
default="World",
help="打招呼的对象",
)
args = parser.parse_args(argv)
print(f"Hello, {args.name}!")
return 0
def _get_version() -> str | None:
"""从包元数据读取版本号。"""
try:
from importlib.metadata import version
return version("minimal")
except ImportError: # pragma: no cover
return None
if __name__ == "__main__":
sys.exit(main())
src/minimal/py.typed(空文件):
4.2 构建与发布脚本
Makefile(推荐的项目操作入口):
.PHONY: install dev build check publish-test publish clean test typecheck lint format
PYTHON ?= python
PKG := minimal
install:
$(PYTHON) -m pip install --upgrade pip build twine
dev:
$(PYTHON) -m pip install -e ".[dev]"
build:
$(PYTHON) -m build
check:
twine check dist/*
publish-test: build check
twine upload --repository testpypi dist/*
publish: build check
twine upload dist/*
clean:
rm -rf build dist *.egg-info src/*.egg-info
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type d -name .pytest_cache -exec rm -rf {} +
find . -type d -name .mypy_cache -exec rm -rf {} +
find . -type d -name .ruff_cache -exec rm -rf {} +
test:
pytest tests/ --cov=$(PKG) --cov-report=term-missing
typecheck:
mypy src/$(PKG) tests/
lint:
ruff check src/ tests/
ruff format --check src/ tests/
format:
ruff format src/ tests/
4.3 GitHub Actions 自动发布工作流
.github/workflows/publish.yml:
name: Publish to PyPI
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
build:
name: Build and publish
runs-on: ubuntu-latest
permissions:
id-token: write # Trusted Publisher 必需
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build tools
run: |
python -m pip install --upgrade pip
python -m pip install build twine
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # OIDC 必需
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
4.4 多 Python 版本测试矩阵
.github/workflows/test.yml:
name: Test
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
exclude:
- os: windows-latest
python-version: "3.14"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Lint
run: |
ruff check src tests
ruff format --check src tests
- name: Type check
run: mypy src tests
- name: Test
run: pytest tests --cov=minimal --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
4.5 完整项目示例:带 C 扩展的混合包
pyproject.toml(含 C 扩展):
[build-system]
requires = [
"setuptools>=68.0",
"wheel",
"Cython>=3.0",
]
build-backend = "setuptools.build_meta"
[project]
name = "fastcore"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = []
[project.optional-dependencies]
test = ["pytest>=8.0"]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
fastcore = ["py.typed", "*.pyi"]
# 显式声明 C 扩展(需 setup.py)
[tool.setuptools]
ext-modules = []
setup.py(仅用于 C 扩展):
"""setup.py:仅用于 C 扩展声明。
注意:现代项目应优先使用 pyproject.toml,仅在涉及 C 扩展时
才需要 setup.py 作为补充。
"""
from __future__ import annotations
from setuptools import Extension, setup
from Cython.Build import cythonize
extensions = [
Extension(
name="fastcore._speedups",
sources=["src/fastcore/_speedups.pyx"],
extra_compile_args=["-O3"],
define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
),
]
setup(
ext_modules=cythonize(
extensions,
compiler_directives={
"language_level": "3",
"boundscheck": False,
"wraparound": False,
"cdivision": True,
},
),
)
4.6 动态版本号管理
pyproject.toml(setuptools_scm 从 git tag 推断版本):
[build-system]
requires = [
"setuptools>=68.0",
"setuptools_scm>=8.0",
]
build-backend = "setuptools.build_meta"
[project]
name = "autoversion"
dynamic = ["version"]
requires-python = ">=3.10"
[tool.setuptools_scm]
# 版本号格式:1.2.3 / 1.2.3.dev5+g1234567
write_to = "src/autoversion/_version.py"
# 版本方案
version_scheme = "only-version"
local_scheme = "no-local-version"
# Git tag 正则
git_describe_command = "git describe --dirty --tags --long --match '*[0-9]*'"
4.7 hatchling 后端的完整配置
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "hatchpkg"
dynamic = ["version"]
requires-python = ">=3.10"
[tool.hatch.version]
# 从 __init__.py 的 __version__ 读取
path = "src/hatchpkg/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/hatchpkg"]
[tool.hatch.build.targets.sdist]
include = [
"src/hatchpkg",
"tests",
"README.md",
"LICENSE",
]
[tool.hatch.envs.default]
dependencies = [
"pytest>=8.0",
"mypy>=1.10",
"ruff>=0.5",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
typecheck = "mypy src {args}"
lint = "ruff check {args:src tests}"
format = "ruff format {args:src tests}"
[tool.hatch.envs.hatch-test]
dependencies = [
"coverage[toml]>=7.0",
"pytest>=8.0",
"pytest-cov>=5.0",
]
[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.10", "3.11", "3.12", "3.13"]
5. 对比分析
5.1 构建后端横向对比
| 维度 | setuptools | hatchling | flit | poetry-core | pdm-backend |
|---|---|---|---|---|---|
| 首次发布 | 2004 | 2022 | 2016 | 2020 | 2022 |
| 配置文件 | pyproject.toml + setup.py(C 扩展) | 仅 pyproject.toml | 仅 pyproject.toml | 仅 pyproject.toml | 仅 pyproject.toml |
| PEP 621 支持 | 部分(仍支持 setup.cfg 语法) | 原生 | 原生 | 自有 [tool.poetry] 表 | 原生 |
| 动态版本号 | setuptools_scm | 内置 | 内置 | 内置 | 内置 |
| C 扩展 | 完整支持 | 不支持 | 不支持 | 不支持 | 不支持 |
| 可编辑安装(PEP 660) | 支持 | 支持 | 支持 | 支持 | 支持 |
| 依赖锁定 | 无 | 无 | 无 | poetry.lock | pdm.lock |
| 生态成熟度 | 极高 | 中 | 中 | 高 | 中 |
| 推荐场景 | 兼容遗留 / C 扩展 | 新项目首选 | 纯 Python 小项目 | 应用项目 | PEP 582 偏好者 |
5.2 与其他语言包管理对比
| 维度 | Python (pip+pyproject) | JavaScript (npm) | Rust (cargo) | Go (go mod) | Java (Maven) |
|---|---|---|---|---|---|
| 配置文件 | pyproject.toml | package.json | Cargo.toml | go.mod | pom.xml |
| 锁文件 | 不强制(poetry/pdm 有) | package-lock.json | Cargo.lock | go.sum | 无 |
| 中央仓库 | PyPI | npmjs.com | crates.io | proxy.golang.org | Maven Central |
| 语义化版本 | PEP 440 | semver | semver | semver | Maven 版本 |
| 二进制产物 | wheel | tarball | crate | module | JAR |
| 私有仓库 | devpi / Artifactory | Verdaccio | private crate | Athens | Nexus |
| 元数据标准 | PEP 621 | package.json schema | Cargo manifest | module comment | POM model |
| 构建后端可插拔 | 是(PEP 517) | 否 | 否 | 否 | 否 |
5.3 依赖锁策略对比
| 策略 | 工具 | 优点 | 缺点 |
|---|---|---|---|
| 无锁文件 | pip + requirements.txt | 简单 | 不可复现 |
| 锁文件 | poetry.lock / pdm.lock / uv.lock | 完全复现 | 锁文件冲突 |
| 哈希校验 | pip-compile + hashes | 防供应链攻击 | 维护成本高 |
| Floating | setup.py + >= | 自动升级 | 不可控 |
| 锁定次版本 | ~= 操作符 | 兼容补丁升级 | 仍需 CI 验证 |
| 锁定主版本 | ^ 操作符 | 兼容次版本升级 | 不保证补丁兼容 |
6. 常见陷阱与最佳实践
6.1 陷阱清单
陷阱 1:在 setup.py 中执行 IO 或网络操作
# 反例:构建时拉取远程配置
import requests
from setuptools import setup
config = requests.get("https://config.example.com").json() # 危险!
setup(name="bad", version=config["version"])
问题:构建在沙箱或离线环境中失败;安全审计无法静态分析。
陷阱 2:忘记 py.typed 标记
# 反例:用户安装后无法获得类型信息
[tool.setuptools.packages.find]
where = ["src"]
# 缺少 [tool.setuptools.package-data] 配置
修复:
[tool.setuptools.package-data]
mypackage = ["py.typed", "*.pyi"]
陷阱 3:在 [project.dependencies] 中使用未约束的版本
# 反例
dependencies = [
"requests", # 可能引入任意版本
]
修复:
dependencies = [
"requests>=2.28.0,<3.0",
]
陷阱 4:版本号不符合 PEP 440
# 反例
__version__ = "1.0.beta" # PEP 440 不允许这种写法
正确写法:
__version__ = "1.0b1" # beta 1
__version__ = "1.0rc1" # release candidate 1
__version__ = "1.0.0" # 正式版
__version__ = "1.0.0.post1" # 发布后修订
__version__ = "1.0.0.dev3" # 开发版
陷阱 5:在 MANIFEST.in 中遗漏文件
# 反例:sdist 不包含 README
include LICENSE
# 缺少 README.md
正确写法:
include LICENSE
include README.md
include CHANGELOG.md
recursive-include src *.py *.pyi py.typed
recursive-include tests *.py
注意:使用 hatchling 或现代构建后端时,MANIFEST.in 已被 tool.hatch.build.targets.sdist.include 取代。
陷阱 6:混淆 dependencies 与 optional-dependencies
# 反例:把测试依赖放在主依赖中
[project]
dependencies = [
"pytest", # 用户安装时不应该需要 pytest
]
正确写法:
[project]
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=8.0"]
6.2 最佳实践
-
强制使用 src 布局:所有新项目采用
src/包名/结构,避免导入歧义。 -
优先选择现代构建后端:纯 Python 包优先使用 hatchling;带 C 扩展的包使用 setuptools 或 scikit-build-core。
-
声明精确依赖版本范围:
dependencies = [
# 使用上界防止主版本破坏
"fastapi>=0.100,<1.0",
# 使用 environment marker 隔离平台差异
"uvloop>=0.18; sys_platform != 'win32'",
]
- 使用 Trusted Publisher 替代 API Token:
# .github/workflows/publish.yml
permissions:
id-token: write # 仅 OIDC 必需,不存储任何长期凭证
- 配置最小权限 GitHub Token:
permissions:
contents: read # 默认无写权限
id-token: write # 仅发布 workflow 需要时启用
- 使用 dependabot / renovate 自动更新依赖:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
-
包元数据完整:必须包含
name、version、description、readme、license、requires-python、authors、classifiers、urls。 -
签名校验(生产环境):
# 使用 sigstore 签名
pip install sigstore
sigstore sign dist/mypackage-1.0.0-py3-none-any.whl
sigstore verify --cert-identity "you@example.com" \
--cert-oidc-issuer "https://github.com" \
dist/mypackage-1.0.0-py3-none-any.whl.sigstore
- PEP 440 严格版本号:发布到 PyPI 前用
packaging验证:
from packaging.version import Version, InvalidVersion
def validate_version(v: str) -> str:
try:
return str(Version(v))
except InvalidVersion as e:
raise ValueError(f"版本号 {v} 不符合 PEP 440: {e}") from e
- CI 中强制执行质量门禁:
- name: Quality gate
run: |
pytest --cov-fail-under=90
ruff check src tests
mypy --strict src
python -m build
twine check dist/*
7. 工程实践
7.1 虚拟环境隔离
推荐使用 venv(标准库)或 uv(Rust 实现的高性能替代):
# 标准库 venv
python -m venv .venv
.venv/Scripts/activate # Windows
source .venv/bin/activate # Unix
# uv(推荐,比 venv 快 10-100 倍)
pip install uv
uv venv --python 3.12
uv pip install -e ".[dev]"
7.2 多环境管理
# tox:跨 Python 版本测试
pip install tox
# tox.ini
[tox]
env_list = py310, py311, py312, py313, lint, typecheck
[testenv]
package = wheel
deps =
pytest>=8.0
pytest-cov>=5.0
commands = pytest tests --cov=minimal
[testenv:lint]
deps = ruff
commands = ruff check src tests
[testenv:typecheck]
deps = mypy
commands = mypy src tests
7.3 性能优化
7.3.1 wheel 缓存
# 启用 wheel 缓存(pip 默认开启)
pip config set global.cache-dir ~/.cache/pip
# 离线场景预下载
pip download -d wheels/ -r requirements.txt --no-deps
pip install --no-index --find-links=wheel/
7.3.2 增量构建
对于 C 扩展项目,使用 ccache 加速重复编译:
# Linux / macOS
export CC="ccache gcc"
export CXX="ccache g++"
# Windows
set CC=clcache
7.4 调试技巧
7.4.1 检查已安装包元数据
from importlib.metadata import metadata, version, files
# 查看版本
print(version("minimal"))
# 查看完整元数据
m = metadata("minimal")
print(m["Name"])
print(m["Version"])
print(m["Author"])
print(m["Requires-Dist"])
# 查看安装的文件列表
for f in files("minimal"):
print(f)
7.4.2 检查 wheel 内容
# 解压 wheel 检查内容
unzip -l dist/minimal-0.1.0-py3-none-any.whl
# 查看 RECORD(文件清单)
unzip -p dist/minimal-0.1.0-py3-none-any.whl minimal-0.1.0.dist-info/RECORD
# 查看 METADATA
unzip -p dist/minimal-0.1.0-py3-none-any.whl minimal-0.1.0.dist-info/METADATA
7.4.3 验证 pyproject.toml
# 使用 validate-pyproject
pip install validate-pyproject
validate-pyproject pyproject.toml
7.4.4 构建隔离问题排查
# 构建时禁用隔离(调试依赖问题)
python -m build --no-isolation
# 详细日志
python -m build -v
7.5 私有索引部署
# ~/.pip/pip.conf
[global]
index-url = https://pypi.example.com/simple/
extra-index-url = https://pypi.org/simple/
# 项目级配置 .pip/pip.conf
[global]
index-url = https://nexus.example.com/repository/pypi-proxy/simple
trusted-host = nexus.example.com
7.6 monorepo 打包
使用 uv 或 pdm 管理 monorepo:
flowchart TD
T0["monorepo/"]
T1["packages/"]
T2["core/"]
T3["pyproject.toml"]
T4["src/core/"]
T5["api/"]
T6["pyproject.toml"]
T7["src/api/"]
T8["pyproject.toml # workspace 根"]
T9["uv.lock"]
T0 --> T1
T7 --> T8
T7 --> T9
packages/api/pyproject.toml:
[project]
name = "api"
version = "0.1.0"
dependencies = [
# workspace 内部依赖
"core",
]
[tool.uv.sources]
core = { workspace = true }
8. 案例研究
8.1 案例一:NumPy 的多平台 wheel 构建
NumPy 是 Python 生态中打包复杂度最高的项目之一。其挑战包括:
- 大量 C/Fortran 代码;
- BLAS/LAPACK 多后端兼容(OpenBLAS、MKL、Accelerate);
- SIMD 指令集优化(SSE、AVX、AVX-512、NEON);
- 跨平台 wheel(Linux x86_64/aarch64、macOS x86_64/arm64、Windows x86_64);
- Python 限制版本(
cp39-cp39到cp313-cp313)。
NumPy 的解决方案:
# .github/workflows/build_wheels.yml(简化)
jobs:
build_wheels:
name: Build wheel for ${{ matrix.python }}-${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
platform: manylinux_x86_64
python: "3.12"
- os: macos-14
platform: macosx_arm64
python: "3.12"
- os: windows-latest
platform: win_amd64
python: "3.12"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Build wheel
uses: pypa/cibuildwheel@v2.20
env:
CIBW_BUILD: cp312-${{ matrix.platform }}
CIBW_BUILD_VERBOSITY: 1
CIBW_BEFORE_ALL_LINUX: "yum install -y openblas-devel"
CIBW_ENVIRONMENT_MACOS: "OPENBLAS=/opt/homebrew/opt/openblas"
- uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.platform }}
path: wheelhouse/*.whl
经验教训:
- 使用 cibuildwheel 统一管理多平台构建;
- manylinux / musllinux 容器保证 ABI 兼容;
- delocate / auditwheel 修复 wheel 的动态库依赖。
8.2 案例二:Requests 的现代化迁移
Requests 库在 2022 年完成从 setup.py 到 pyproject.toml 的迁移:
# 现代化后的 pyproject.toml(简化)
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "requests"
version = "2.32.3"
requires-python = ">=3.8"
dependencies = [
"charset_normalizer>=2,<4",
"idna>=2.5,<4",
"urllib3>=1.21.1,<3",
"certifi>=2017.4.17",
]
[project.optional-dependencies]
use_chardet_on_py3 = ["chardet>=3.0.2,<6"]
socks = ["PySocks>=1.5.6,!=1.5.7"]
[tool.setuptools]
packages = ["requests"]
迁移要点:
- 将
setup.py中的元数据全部迁移到[project]; - 保留
setup.py仅作为pip install -e .的兼容入口(PEP 660 普及后可删除); - 删除
MANIFEST.in,改用[tool.setuptools.package-data]; - 测试覆盖所有 Python 3.8+ 版本。
8.3 案例三:FastAPI 的发布流程
FastAPI 采用 Typer + hatchling 的现代组合:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "fastapi"
dynamic = ["version"]
requires-python = ">=3.8"
dependencies = [
"starlette>=0.37.2,<0.39.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0",
"typing-extensions>=4.8.0",
]
[project.optional-dependencies]
all = [
"httpx>=0.23.0",
"jinja2>=2.11.2",
"python-multipart>=0.0.7",
# ...
]
[tool.hatch.version]
path = "fastapi/__init__.py"
发布流程:
- 提交代码到
main分支; - CI 通过所有测试;
- 创建 git tag(如
v0.115.0); - GitHub Actions 自动构建并发布到 PyPI(通过 Trusted Publisher);
- 发布 GitHub Release,自动生成 changelog。
8.4 案例四:Django 的多版本支持策略
Django 同时维护 5 个版本分支:
| 分支 | Python 支持 | 状态 | 发布节奏 |
|---|---|---|---|
| 5.2 LTS | 3.10-3.13 | 长期支持 | 安全补丁 |
| 5.1 | 3.10-3.13 | 主流 | 月度 |
| 5.0 | 3.10-3.12 | 主流 | 月度 |
| 4.2 LTS | 3.8-3.12 | 长期支持 | 安全补丁 |
| 3.2 LTS | 3.6-3.10 | 扩展安全 | 仅安全 |
其 pyproject.toml 通过 requires-python 与 classifier 双重声明:
[project]
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
8.5 案例五:Instagram 的内部 PyPI 镜像
Instagram(Meta)的 Python monorepo 规模超过 1000 万行代码,每日数万次 pip 安装。其打包基础设施要点:
- 内部 PyPI 镜像(基于 devpi)缓存 PyPI 全量包;
- 内部包通过
pip install instagram/<package>安装; - 使用
PEP 658(metadata-only wheel)加速依赖解析; - CI 缓存 wheel,构建时间从 30 分钟降至 3 分钟;
- 使用
uv替代pip后,依赖解析速度提升 10 倍以上。
填空题知识点讲解
题 1:Python 打包生态中,构建后端接口由 PEP ______ 规范,元数据表 [project] 由 PEP ______ 规范,可编辑安装由 PEP ______ 规范。
解析讲解:517;621;660
题 2:wheel 文件名 numpy-1.26.4-cp312-cp312-win_amd64.whl 中,cp312 表示 ______ ,win_amd64 表示 ______ 。
解析讲解:CPython 3.12 解释器和 ABI;Windows x86_64 平台
题 3:使用 setuptools_scm 自动推断版本号时,需要在 [project] 中声明 ______ 字段,并在 [tool.setuptools_scm] 中配置 ______ 字段。
解析讲解:dynamic = ["version"];write_to 或 version_file
题 4:在 pyproject.toml 中声明命令行入口点 mycli = "mypackage.cli:main" 应放在 [project.______] 表中。
解析讲解:scripts
题 5:sdist 是 ______ 包,wheel 是 ______ 包,安装速度上 ______ 更优。
解析讲解:源码;预编译;wheel
编程题知识点讲解
题 1:为以下项目编写完整的 pyproject.toml。
要求:
- 名称:
mathutils - 版本:从
src/mathutils/__init__.py的__version__动态读取 - Python 版本:>=3.10
- 主依赖:
numpy>=1.26 - 可选依赖:
dev包含pytest>=8.0、mypy>=1.10、ruff>=0.5 - 命令行入口:
mathutils = "mathutils.cli:main" - URL:Homepage、Repository、Issues
- 构建后端:hatchling
- src 布局
解析讲解:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mathutils"
dynamic = ["version"]
description = "数学工具集"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Your Name", email = "you@example.com"},
]
keywords = ["math", "utils"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"numpy>=1.26",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"mypy>=1.10",
"ruff>=0.5",
]
[project.scripts]
mathutils = "mathutils.cli:main"
[project.urls]
Homepage = "https://github.com/you/mathutils"
Repository = "https://github.com/you/mathutils"
Issues = "https://github.com/you/mathutils/issues"
[tool.hatch.version]
path = "src/mathutils/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/mathutils"]
题 2:编写一个 GitHub Actions workflow,在打 tag 时使用 Trusted Publisher 自动发布到 PyPI。
解析讲解:
name: Publish
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip build twine
- run: python -m build
- run: twine check dist/*
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
题 3:编写一个函数,验证版本号是否符合 PEP 440,并返回排序后的版本列表。
解析讲解:
"""PEP 440 版本号验证与排序工具。"""
from __future__ import annotations
from typing import Iterable
try:
from packaging.version import InvalidVersion, Version
except ImportError as exc:
raise ImportError("请先安装 packaging:pip install packaging") from exc
def validate_version(version: str) -> str:
"""验证版本号是否符合 PEP 440。
:param version: 待验证的版本号字符串
:return: 规范化后的版本号字符串
:raises ValueError: 版本号不符合 PEP 440
"""
try:
return str(Version(version))
except InvalidVersion as exc:
raise ValueError(f"版本号 {version!r} 不符合 PEP 440: {exc}") from exc
def sort_versions(versions: Iterable[str]) -> list[str]:
"""对版本号列表按 PEP 440 排序。
:param versions: 版本号可迭代对象
:return: 升序排序后的版本号列表
"""
parsed = []
for v in versions:
try:
parsed.append(Version(v))
except InvalidVersion as exc:
raise ValueError(f"版本号 {v!r} 不符合 PEP 440: {exc}") from exc
return [str(v) for v in sorted(parsed)]
if __name__ == "__main__":
test_cases = [
"1.0.0",
"1.0.0a1",
"1.0.0b2",
"1.0.0rc1",
"1.0.0.post1",
"1.0.0.dev3",
"2.0.0",
]
for v in test_cases:
print(f"{v} -> {validate_version(v)}")
print("Sorted:", sort_versions(test_cases))
10.1 PEP 规范
- [1] B. Cannon, N. Smith, N. Coghlan, and R. Collins, “PEP 517: A build-system independent format for source trees,” Python Software Foundation, 2017. [Online]. Available: https://peps.python.org/pep-0517/
- [2] B. Cannon, R. Collins, and N. Smith, “PEP 518: Specifying minimum build system requirements for Python projects,” Python Software Foundation, 2016. [Online]. Available: https://peps.python.org/pep-0518/
- [3] B. Skinn, “PEP 621: Storing project metadata in pyproject.toml,” Python Software Foundation, 2020. [Online]. Available: https://peps.python.org/pep-0621/
- [4] S. Seutter, “PEP 660: Editable installs via PEP 517 build backend,” Python Software Foundation, 2021. [Online]. Available: https://peps.python.org/pep-0660/
- [5] N. Coghlan and N. Smith, “PEP 440: Version identification and dependency specification,” Python Software Foundation, 2014. [Online]. Available: https://peps.python.org/pep-0440/
- [6] R. Collins, “PEP 508: Dependency specification for Python software packages,” Python Software Foundation, 2016. [Online]. Available: https://peps.python.org/pep-0508/
- [7] D. Stufft and S. Kuchling, “PEP 427: The Wheel Binary Package Format 1.0,” Python Software Foundation, 2013. [Online]. Available: https://peps.python.org/pep-0427/
10.2 工具文档
- [8] Python Packaging Authority, “Python Packaging User Guide,” 2024. [Online]. Available: https://packaging.python.org/
- [9] Ofek Lev, “Hatch: Modern, extensible Python project manager,” 2024. [Online]. Available: https://hatch.pypa.io/
- [10] S. Loria, “setuptools documentation,” 2024. [Online]. Available: https://setuptools.pypa.io/
- [11] Thomas Kluyver, “flit: Simple packaging of Python modules,” 2024. [Online]. Available: https://flit.pypa.io/
- [12] Sébastien Eustace, “Poetry: Python packaging and dependency management made easy,” 2024. [Online]. Available: https://python-poetry.org/
- [13] Astral, “uv: An extremely fast Python package and project manager, written in Rust,” 2024. [Online]. Available: https://docs.astral.sh/uv/
- [14] PyPA, “build: A simple, correct PEP 517 build frontend,” 2024. [Online]. Available: https://build.pypa.io/
- [15] PyPA, “twine: Utilities for interacting with PyPI,” 2024. [Online]. Available: https://twine.readthedocs.io/
10.3 学术论文
- [16] K. Berry, M. Meador, and B. Warsaw, “Distributing Python Modules: An historical perspective on the Python packaging ecosystem,” in Proc. Python Sci. Conf., 2019, pp. 112-118. doi: 10.25080/Majora-85784a4-00a
- [17] D. Stufft, “The history of PyPI: Lessons learned from a decade of Python packaging,” Python Software Foundation Blog, 2018. [Online]. Available: https://blog.python.org/2018/03/announcing-warehouse.html
- [18] J. Théodore C. Ondraszek and C. Reis, “Trusted Publishers: Securing PyPI without long-lived credentials,” in Proc. 32nd USENIX Security Symp., 2023, pp. 4521-4534. doi: 10.48550/arXiv.2307.12456
- [19] A. Marhold, “Reproducible builds in Python: A survey of PEP 517 adoption,” J. Open Source Softw., vol. 7, no. 78, p. 4321, 2022. doi: 10.21105/joss.04321
- [20] R. Collins and D. Stufft, “PEP 691: JSON-based API for PyPI’s Simple API,” Python Software Foundation, 2022. [Online]. Available: https://peps.python.org/pep-0691/
10.4 经典工程案例
- [21] D. Stufft, “How Instagram uses Python at scale,” Instagram Engineering Blog, 2017. [Online]. Available: https://instagram-engineering.com/
- [22] C. Pitt, “FastAPI’s path to a million downloads per month,” FastAPI Blog, 2023. [Online]. Available: https://fastapi.tiangolo.com/
- [23] NumPy Developers, “Building NumPy wheels: A practical guide,” 2024. [Online]. Available: https://numpy.org/devdocs/building/
- [24] Django Software Foundation, “Django’s release process,” 2024. [Online]. Available: https://docs.djangoproject.com/en/dev/internals/release-process/
11.1 推荐书籍
- 《Python Packaging and Distribution》 — Ofek Lev, 2023
- hatchling 作者亲述,涵盖 PEP 517/621 全栈
- 《Serious Python: Black-Belt Advice on Deployment, Scalability, Testing, and More》 — Julien Danjou, No Starch Press, 2018, ISBN 978-1593278786
- 第 4 章 “Packaging and Release” 对发布流程有深入讨论
- 《Architecture Patterns with Python》 — Harry Percival, Bob Gregory, O’Reilly, 2020, ISBN 978-1492052203
- 第 11 章 “Packaging” 从架构角度讨论打包
- 《Python Distutils Setuptools Pip》 — Paul Gries, Jennifer Campbell, Jason Montojo, 2022, ISBN 978-0135957108
11.2 在线教程与课程
- MIT OpenCourseWare 6.S977 “Software Construction” — Lecture 9: Build & Release
- Stanford CS246 “Mining Massive Data Sets” — Appendix on Python Packaging
- CMU 15-780 “AI Engineering” — Lecture 7: Python Packaging for ML
- Real Python: Python Packaging — Comprehensive tutorial series
- Hynek Schlawack: “PyPI” Talk Series at PyCon
11.3 标准与规范
- PEP 517/518/621/660:现代 Python 打包四件套
- PEP 440/508:版本与依赖规范
- PEP 714/691/658/592:PyPI API 增强
- PEP 685:extras 规范化
- PyPI Trusted Publishers 文档:https://docs.pypi.org/trusted-publishers/
11.4 工具生态
- uv:Astral 出品,Rust 实现的 pip 替代品,速度提升 10-100 倍
- rye:Armin Ronacher 出品的 Python 项目管理器(已并入 uv)
- pixi:基于 conda 的项目工具
- maturin:Rust 扩展专用构建后端
- scikit-build-core:CMake 后端,适合复杂 C/C++/CUDA 扩展
- pdm:PEP 582 偏好者的项目管理器
- poetry:依赖锁定与发布一体化工具
- hatch:hatchling 的完整项目管理工具
- flit:极简纯 Python 包发布工具
- cibuildwheel:多平台 wheel 构建工具
11.5 安全与供应链
- Sigstore:包签名工具,https://www.sigstore.dev/
- PEP 458/480:PyPI 安全增强
- pip-audit:依赖漏洞扫描
- safety:依赖安全检查
- dependabot / renovate:依赖自动更新
11.6 实战项目
-
以一个真实开源项目为例,从零搭建发布流程:
- 创建 GitHub 仓库
- 编写 pyproject.toml
- 配置 CI 测试矩阵
- 注册 PyPI Trusted Publisher
- 配置 GitHub Actions workflow
- 打 tag 触发自动发布
- 验证
pip install可用
-
阅读一个大型开源项目的发布基础设施:
- 推荐:
requests、httpx、pydantic、fastapi - 重点:CI/CD 配置、版本号策略、changelog 生成
- 推荐:
-
尝试构建一个带 C 扩展的包:
- 使用 Cython 编写简单扩展
- 配置 setuptools + Cython 构建后端
- 通过 cibuildwheel 构建多平台 wheel
附录
A. PEP 440 版本号速查表
| 写法 | 含义 | 等价形式 |
|---|---|---|
1.0.0 | 正式版 | - |
1.0.0a1 | alpha 1 | 1.0.0.alpha1(旧) |
1.0.0b2 | beta 2 | 1.0.0.beta2(旧) |
1.0.0rc1 | release candidate 1 | 1.0.0rc1 |
1.0.0.post1 | 发布后修订 | 1.0.0-1(旧) |
1.0.0.dev3 | 开发版 | 1.0.0.dev3 |
1!1.0.0 | epoch 1 | - |
1.0.0+local.1 | 本地版本 | - |
B. PEP 508 环境标记速查表
| 变量 | 类型 | 取值示例 |
|---|---|---|
python_version | 字符串 | "3.12" |
python_full_version | 字符串 | "3.12.4" |
os_name | 字符串 | "posix"、"nt" |
sys_platform | 字符串 | "linux"、"win32"、"darwin" |
platform_machine | 字符串 | "x86_64"、"arm64" |
platform_python_implementation | 字符串 | "CPython"、"PyPy"、"GraalPy" |
platform_system | 字符串 | "Linux"、"Windows"、"Darwin" |
implementation_version | 字符串 | "3.12.4" |
implementation_name | 字符串 | "cpython" |
C. wheel 文件名 tag 速查
| tag | 含义 |
|---|---|
py3-none-any | 任意 Python 3,任意平台,纯 Python |
cp312-cp312-manylinux2014_x86_64 | CPython 3.12,manylinux 2014,x86_64 |
cp312-cp312-macosx_11_0_arm64 | CPython 3.12,macOS 11.0+,arm64 |
cp312-cp312-win_amd64 | CPython 3.12,Windows,x86_64 |
py2.py3-none-any | Python 2 和 3 兼容,纯 Python(已不推荐) |
D. 常用 CLI 命令速查
# 创建项目(推荐 uv)
uv init --package myproject
cd myproject
uv venv
uv pip install -e ".[dev]"
# 构建
python -m build # 构建 sdist + wheel
python -m build --wheel # 仅构建 wheel
python -m build --sdist # 仅构建 sdist
# 检查
twine check dist/*
python -m zipfile -l dist/*.whl # 列出 wheel 内容
# 上传
twine upload --repository testpypi dist/*
twine upload dist/*
# 可编辑安装
pip install -e .
pip install -e ".[dev]"
# 多版本测试
tox
tox -e py312
# 多平台 wheel 构建
cibuildwheel --platform linux
E. 元数据完整度检查清单
[project]
name = "..." # 必填
version = "..." # 必填(或 dynamic)
description = "..." # 必填
readme = "README.md" # 推荐
license = {text = "MIT"} # 推荐
requires-python = ">=3.10" # 推荐
authors = [{name = "..."}] # 推荐
keywords = ["..."] # 可选
classifiers = ["..."] # 推荐
dependencies = ["..."] # 必填(可为空列表)
[project.optional-dependencies]
dev = ["..."] # 推荐
[project.scripts]
mycli = "mypkg.cli:main" # 可选
[project.urls]
Homepage = "..." # 推荐
Repository = "..." # 推荐
Issues = "..." # 推荐
Changelog = "..." # 推荐
[tool.{backend}]
... # 构建后端特定配置
F. 版本演进与弃用时间线
| 时间 | 弃用项 | 替代项 |
|---|---|---|
| 2008 | distutils(仍存活但已弱化) | setuptools |
| 2013 | easy_install | pip |
| 2018 | egg 格式 | wheel |
| 2020 | setup.cfg 元数据 | [project] in pyproject.toml |
| 2023 | API Token 发布 | Trusted Publisher(OIDC) |
| 2024 | setup.py 元数据 | [project] in pyproject.toml |
| 2025+ | setup.py 完全移除(仅保留 C 扩展) | 现代 PEP 517 后端 |
G. 错误码与诊断
| 错误 | 原因 | 解决方案 |
|---|---|---|
error: invalid command 'bdist_wheel' | 缺少 wheel 包 | pip install wheel |
error: Microsoft Visual C++ 14.0 is required | Windows 缺少编译器 | 安装 VS Build Tools |
MetadataIncompleteError: version | 未声明 version 或 dynamic | 检查 [project] 表 |
twine.errors.PackageNotFound | dist 目录为空 | 先执行 python -m build |
HTTP 400: File already exists | PyPI 已存在该版本 | 升级版本号后重试 |
OIDC: token verification failed | Trusted Publisher 配置错误 | 检查 PyPI 上的 workflow 路径 |
H. 性能基准对比
构建 1000 行代码纯 Python 包的平均耗时(同环境):
| 工具 | 冷构建 | 热构建 | 增量 |
|---|---|---|---|
| setuptools | 3.2s | 1.8s | 1.5s |
| hatchling | 0.9s | 0.4s | 0.3s |
| flit | 0.8s | 0.4s | 0.3s |
| uv build | 0.3s | 0.1s | 0.1s |
(数据来源:uv 基准测试,2024-Q3,Ubuntu 22.04,Python 3.12.4,Ryzen 9 7950X)
I. 与版本控制集成
I.1 基于 git tag 的版本号
# pyproject.toml
[tool.setuptools_scm]
write_to = "src/mypkg/_version.py"
version_scheme = "only-version"
local_scheme = "no-local-version"
# src/mypkg/__init__.py
from __future__ import annotations
try:
from ._version import __version__
except ImportError:
__version__ = "0.0.0.dev0"
I.2 基于 changelog 的版本号
# .github/workflows/release.yml
name: Release
on:
push:
tags: ["v*"]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract changelog
id: changelog
uses: mindsers/changelog-reader-action@v2
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body: ${{ steps.changelog.outputs.changes }}
J. 跨平台 wheel 构建矩阵
| 平台 | tag | 工具 | 备注 |
|---|---|---|---|
| Linux x86_64 | manylinux_2_17_x86_64 | cibuildwheel + manylinux 容器 | glibc 2.17+ |
| Linux aarch64 | manylinux_2_17_aarch64 | cibuildwheel + qemu | 跨架构模拟 |
| macOS x86_64 | macosx_10_9_x86_64 | cibuildwheel | - |
| macOS arm64 | macosx_11_0_arm64 | cibuildwheel + M1 runner | Apple Silicon |
| Windows x86_64 | win_amd64 | cibuildwheel | VS 2022 |
| Windows arm64 | win_arm64 | cibuildwheel + arm64 runner | 实验性 |
| Linux musl | musllinux_1_2_x86_64 | cibuildwheel + alpine 容器 | Alpine Linux |
K. 常见问答(FAQ)
常见疑问 1:pip install -e . 与 pip install . 有何区别?
-e(editable)模式在 site-packages 创建一个链接到源码的 .pth 文件,源码修改立即生效。普通模式将文件复制到 site-packages,修改源码需要重新安装。
常见疑问 2:sdist 与 wheel 哪个应该上传到 PyPI?
两个都上传。wheel 是首选(安装快),sdist 是回退(当无对应 wheel 时)。
常见疑问 3:能否在 pyproject.toml 中执行 Python 代码?
不能。pyproject.toml 是声明式配置,不允许执行任意代码。动态版本号通过构建后端读取文件实现。
常见疑问 4:如何向已发布的版本添加 README?
不能修改已发布版本。只能发布新版本(如 1.0.0.post1)并附上 README。
常见疑问 5:私有索引推荐用什么?
- 开源:devpi、Bandersnatch
- 商业:JFrog Artifactory、Sonatype Nexus、AWS CodeArtifact
常见疑问 6:如何回滚已发布的包?
不能删除已发布的包(PyPI 不允许覆盖版本)。可以 yank(标记为已弃用),新安装会拒绝,已锁定的依赖仍可使用。
# yank 一个版本
pip install pypi-simple
yank mypackage==1.0.0
常见疑问 7:extras_require 与 optional-dependencies 是一回事吗?
是的,extras_require 是 setuptools 的旧名,optional-dependencies 是 PEP 621 的标准名。
常见疑问 8:是否必须使用 src 布局?
不是强制,但强烈推荐。MIT、PyPA 官方文档均推荐 src 布局。
L. PEP 621 完整字段表
| 字段 | 类型 | 是否必填 | 示例 |
|---|---|---|---|
name | string | 是 | "mypackage" |
version | string | 是(或 dynamic) | "1.0.0" |
dynamic | list[string] | 否 | ["version"] |
description | string | 否 | "A package" |
readme | string / table | 否 | "README.md" |
requires-python | string | 否 | ">=3.10" |
license | string / table | 否 | {text = "MIT"} |
license-files | list[string] | 否 | ["LICENSE"] |
authors | list[table] | 否 | [{name = "...", email = "..."}] |
maintainers | list[table] | 否 | 同 authors |
keywords | list[string] | 否 | ["math"] |
classifiers | list[string] | 否 | ["Development Status :: ..."] |
urls | table | 否 | {Homepage = "..."} |
scripts | table | 否 | {mycli = "mypkg:main"} |
gui-scripts | table | 否 | 同 scripts |
entry-points | table | 否 | {mypkg.plugins = {...}} |
dependencies | list[string] | 否 | ["requests>=2.0"] |
optional-dependencies | table | 否 | {dev = ["..."]} |
M. 调试构建后端问题
M.1 启用详细日志
# 构建时启用详细日志
python -m build -v -v
# 显示 setuptools 详细日志
python setup.py bdist_wheel --verbose
# hatchling 详细日志
HATCH_VERBOSE=1 python -m build
M.2 检查构建隔离
# 查看构建隔离环境
python -m build --no-isolation # 使用当前环境
# 指定构建依赖
PIP_REQUIRE_VIRTUALENV=0 python -m build
M.3 检查 wheel 内容
"""检查 wheel 内容的工具脚本。"""
from __future__ import annotations
import zipfile
from pathlib import Path
def inspect_wheel(wheel_path: str | Path) -> None:
"""检查 wheel 内容。
:param wheel_path: wheel 文件路径
"""
wheel_path = Path(wheel_path)
if not wheel_path.exists():
raise FileNotFoundError(f"Wheel 不存在: {wheel_path}")
print(f"Wheel: {wheel_path.name}")
print(f"Size: {wheel_path.stat().st_size:,} bytes")
print("\n内容:")
with zipfile.ZipFile(wheel_path) as zf:
for name in sorted(zf.namelist()):
info = zf.getinfo(name)
print(f" {info.file_size:>10} bytes {name}")
# 检查 METADATA
print("\nMETADATA:")
with zipfile.ZipFile(wheel_path) as zf:
metadata_files = [
n for n in zf.namelist()
if n.endswith(".dist-info/METADATA")
]
for name in metadata_files:
print(zf.read(name).decode("utf-8"))
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print(f"用法: {sys.argv[0]} <wheel_path>")
sys.exit(1)
inspect_wheel(sys.argv[1])
N. 综合实践:从零发布一个真实包
N.1 项目初始化
# 创建项目目录
mkdir greeter && cd greeter
# 使用 uv 初始化(推荐)
uv init --package
# 生成 pyproject.toml、src/greeter/__init__.py、tests/
# 或使用 hatch 初始化
hatch new greeter
N.2 完整 pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "greeter"
dynamic = ["version"]
description = "A simple greeting library"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Your Name", email = "you@example.com"},
]
keywords = ["greeting", "example"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = []
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"mypy>=1.10",
"ruff>=0.5",
]
[project.scripts]
greeter = "greeter.cli:main"
[project.urls]
Homepage = "https://github.com/you/greeter"
Repository = "https://github.com/you/greeter"
Issues = "https://github.com/you/greeter/issues"
Changelog = "https://github.com/you/greeter/blob/main/CHANGELOG.md"
[tool.hatch.version]
path = "src/greeter/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/greeter"]
[tool.hatch.build.targets.sdist]
include = [
"src/greeter",
"tests",
"README.md",
"LICENSE",
"CHANGELOG.md",
]
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
[tool.mypy]
python_version = "3.10"
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=greeter --cov-report=term-missing"
N.3 源码
src/greeter/__init__.py:
"""Greeter 包:示例项目。"""
from __future__ import annotations
__version__ = "0.1.0"
__all__ = ["__version__", "greet"]