pyproject.toml 与打包分发 (Packaging & Distribution)


章节概述

C 语言项目有 MakefileCMakeLists.txt 定义如何编译代码,Python 项目有 pyproject.toml 定义项目的元数据、依赖关系和构建方式。pyproject.toml(PEP 518/621)是现代 Python 打包的标准入口——它统一了项目配置,结束了过去 setup.pysetup.cfgMANIFEST.inrequirements.txt 各自为政的混乱局面。

本章从 C 程序员视角出发,将 pyproject.tomlCMakeLists.txt 逐项对比,讲解 Python 项目的构建、打包和分发全流程。

核心理念:C 项目的”产出”是可执行文件或 .so/.a 库文件,Python 项目的”产出”是 wheel(.whl)或源码分发包(.tar.gz)。pyproject.toml 之于 Python 项目,就如 CMakeLists.txt 之于 C 项目——它描述”如何构建”和”产物是什么”。


第一节:pyproject.toml 结构详解

1.1 最小化 pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
 
[project]
name = "my-project"
version = "0.1.0"
description = "A sample Python project"
requires-python = ">=3.9"

仅此 8 行就定义了一个可安装的 Python 包。对比 C 项目的最小 CMakeLists.txt 至少也需要 cmake_minimum_required + project + add_executable 三行。

1.2 完整项目元数据

[project]
name = "my-c-hybrid-tools"
version = "1.2.0"
description = "Build helpers and test tools for C projects"
readme = "README.md"
license = {text = "MIT"}
authors = [
 {name = "Your Name", email = "you@example.com"}
]
keywords = ["c", "build-tools", "testing"]
classifiers = [
 "Development Status :: 4 - Beta",
 "Programming Language :: Python :: 3",
 "Programming Language :: Python :: 3.9",
 "Programming Language :: Python :: 3.10",
 "Programming Language :: Python :: 3.11",
 "Programming Language :: Python :: 3.12",
 "Operating System :: OS Independent",
]
requires-python = ">=3.9"
 
# 依赖声明
dependencies = [
 "click>=8.0",
 "pyyaml>=6.0",
 "rich>=13.0",
]
 
# 可选依赖(按功能分组)
[project.optional-dependencies]
dev = [
 "pytest>=8.0",
 "ruff>=0.4",
 "mypy>=1.0",
]
docs = [
 "sphinx>=7.0",
 "myst-parser>=2.0",
]

与 C 对比:CMakeLists.txtfind_package(foo REQUIRED) 对应 dependenciesproject.optional-dependencies 对应 CMake 的 option() + 条件 find_package

1.3 命令行入口点

[project.scripts]
# 安装后,直接在终端调用 mytool 命令
mytool = "my_project.cli:main"
build-helper = "my_project.build_helper:run"

安装后效果:

pip install .
# 现在可以直接运行:
mytool --help
build-helper --input config.json

与 C 对比:[project.scripts] 等价于 CMake 中的 install(TARGETS mytool RUNTIME DESTINATION bin)。两者都将可执行入口安装到 $PATH 中。

1.4 完整的 pyproject.toml 示例

[build-system]
requires = ["hatchling>=1.18"]
build-backend = "hatchling.build"
 
[project]
name = "c-project-tools"
version = "0.1.0"
description = "Tools for managing C projects with Python"
requires-python = ">=3.9"
dependencies = ["click>=8.0", "jinja2>=3.0"]
readme = "README.md"
 
[project.scripts]
cgen = "c_project_tools.cli:main"
 
[project.optional-dependencies]
test = ["pytest>=8.0", "pytest-cov>=5.0"]
lint = ["ruff>=0.4", "mypy>=1.0"]
 
[tool.ruff]
line-length = 100
target-version = "py39"
 
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
 
[tool.mypy]
python_version = "3.9"
strict = true
ignore_missing_imports = true
 
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = "-ra -q --strict-markers"

[tool.*] 配置段是 pyproject.toml 的另一大优势——把 ruff、mypy、pytest、coverage 等工具的配置全部集中在同一个文件中,类似于 CMakeLists.txt 集中管理 C 项目的编译选项、测试设置、安装规则。


第二节:构建后端对比

2.1 三大主流构建后端

后端速度复杂度配置方式适用场景
hatchling纯 pyproject.toml纯 Python 包,推荐首选
setuptoolspyproject.toml + setup.cfg含 C 扩展的传统项目
flit极快极低纯 pyproject.toml纯 Python 小包
# hatchling —— 新一代默认选择
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
 
# setuptools —— 传统选择(需 C 扩展时)
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
 
# flit —— 极简选择
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"

2.2 hatchling:推荐的新项目选择

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
 
[project]
name = "mylib"
version = "0.1.0"
 
[tool.hatch.build.targets.wheel]
packages = ["src/mylib"] # 仅打包 src/mylib 目录

hatchling 默认使用 Python 包发现机制,自动识别 src/ 布局:

graph TB
 PROJ["project/"]
 PROJ --> PPROJ["pyproject.toml"]
 PROJ --> SRC["src/"]
 SRC --> MYLIB["mylib/"]
 MYLIB --> INIT["__init__.py"]
 MYLIB --> CORE["core.py"]
 MYLIB --> UTILS["utils.py"]

2.3 setuptools:兼容包含 C 扩展的包

[tool.setuptools.packages.find]
where = ["src"]
 
[tool.setuptools.package-data]
mylib = ["*.so", "*.dll", "*.dylib"]

如果你的 Python 包通过 ctypes 或 cffi 调用 C 预编译库,setuptools 能将 .so 文件一同打包进 wheel。


第三节:构建产物 —— wheel 与 sdist

3.1 构建命令

# 安装构建工具
pip install build
 
# 构建 wheel 和源码分发包
python -m build
 
# 产物在 dist/ 目录下
ls dist/
# my_project-0.1.0-py3-none-any.whl ← wheel(预编译包)
# my_project-0.1.0.tar.gz ← sdist(源码分发包)

3.2 wheel 文件名解析

my_project-0.1.0-py3-none-any.whl
 ^^^^^^^^ ^^^ ^^^ ^^^^ ^^^
 包名 版本 Python ABI 平台
 版本
字段含义示例
py3Python 3 所有子版本兼容py3 = Python 3.x
none无 C ABI 依赖(纯 Python)none / cp312
any任意平台any / linux_x86_64 / macosx_14_0_arm64

包含 C 扩展的 wheel:

numpy-1.26.4-cp312-cp312-manylinux_2_28_x86_64.whl
# cp312 = CPython 3.12 的 ABI
# manylinux = 兼容多发行版的 Linux x86_64

3.3 安装方式对比

# 从 PyPI 安装
pip install my-project
 
# 从本地 wheel 安装
pip install dist/my_project-0.1.0-py3-none-any.whl
 
# 开发模式安装(源码修改立即生效)
pip install -e .
 
# 从 Git 仓库安装
pip install git+https://github.com/user/repo.git

与 C 对比:pip install -e . 相当于 C 项目的 make && make install(源码方式),而 wheel 安装相当于 apt install xxx.deb(预编译包)。-e(editable)模式创建指向源码目录的链接,修改 .py 文件无需重新安装——这比 C 的增量编译更直接,因为 Python 没有编译步骤。


第四节:pyproject.toml vs CMakeLists.txt 全面对比

概念Python (pyproject.toml)C (CMakeLists.txt)
项目名[project] name = "foo"project(Foo C)
版本version = "1.0.0"project(Foo VERSION 1.0.0)
依赖dependencies = ["lib>=1.0"]find_package(lib 1.0 REQUIRED)
构建目标[project.scripts]add_executable(foo main.c)
头文件/包路径[tool.setuptools.packages.find]target_include_directories
可选功能[project.optional-dependencies]option(BUILD_TESTS "..." ON)
测试[tool.pytest.ini_options]enable_testing() + add_test()
安装规则自动(pip 处理)install(TARGETS ...)
构建配置[tool.*]set(CMAKE_C_FLAGS ...)
C 扩展[tool.setuptools] ext_modulesadd_library(foo SHARED ...)

4.1 混合项目的 pyproject.toml

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
 
[project]
name = "cfib-wrapper"
version = "0.1.0"
description = "Python wrapper for C fibonacci library"
requires-python = ">=3.9"
dependencies = []
 
[project.scripts]
cfib = "cfib_wrapper.cli:main"
 
[tool.setuptools.packages.find]
where = ["src"]

对应的 CMakeLists.txt 可以这样配合:

cmake_minimum_required(VERSION 3.18)
project(CFib C)
 
add_library(fib SHARED src/fib.c)
set_target_properties(fib PROPERTIES
 LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/src/cfib_wrapper"
)

这种混合项目中,CMake 负责编译 C 共享库,pyproject.toml 负责打包 Python 代码和编译好的 .so


练习

以下题目用于验证本章所学内容:

题号题目链接涉及知识点
本章无对应力扣题请用动手练习题自检