从零开始:使用 pyproject.toml 构建现代化 Python 项目

前言
如果你曾经接触过 Python 项目,可能对 setup.py、setup.cfg、requirements.txt 这些文件并不陌生。但随着 Python 生态的发展,这种分散的配置方式逐渐暴露出诸多问题:配置分散、格式不统一、工具兼容性差等。
pyproject.toml 的出现正是为了解决这些痛点。它是 Python 项目的现代化配置标准,由 PEP 518、PEP 621 等规范定义,旨在提供一个统一的、声明式的项目配置文件。
本文将以一个实际项目 PyImage Split(图片拆分工具)为例,详细讲解如何从零开始配置 pyproject.toml,让你的 Python 项目更加规范、现代化。
目录
为什么选择 pyproject.toml
传统方式的问题
在 pyproject.toml 出现之前,一个典型的 Python 项目可能包含以下配置文件:
my_project/
├── setup.py # 项目元数据和构建配置
├── setup.cfg # 部分配置的声明式版本
├── requirements.txt # 运行依赖
├── requirements-dev.txt # 开发依赖
├── MANIFEST.in # 打包文件清单
├── pytest.ini # pytest 配置
├── .flake8 # flake8 配置
├── .isort.cfg # isort 配置
└── mypy.ini # mypy 配置
这种方式存在以下问题:
- 配置分散:相关配置散落在多个文件中,难以维护
- 格式不统一:INI、CFG、TXT、Python 脚本混杂
- setup.py 的安全隐患:作为 Python 脚本,可能执行任意代码
- 依赖管理混乱:运行依赖和开发依赖分离管理
pyproject.toml 的优势
- 统一配置:所有配置集中在一个文件
- 标准格式:使用 TOML 格式,语法清晰易读
- 声明式配置:无需执行代码,更安全
- 工具兼容:现代 Python 工具都支持
- PEP 标准:官方推荐的配置方式
pyproject.toml 的基本结构
一个完整的 pyproject.toml 通常包含以下几个主要部分:
# 1. 构建系统配置
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
# 2. 项目元数据
[project]
name = "my-project"
version = "0.1.0"
# ... 其他元数据
# 3. 可选依赖
[project.optional-dependencies]
dev = ["pytest", "black"]
# 4. 入口点
[project.scripts]
my-command = "my_package.main:main"
# 5. 项目链接
[project.urls]
Homepage = "https://github.com/..."
# 6. 构建工具特定配置
[tool.setuptools]
# setuptools 相关配置
# 7. 其他工具配置
[tool.black]
line-length = 88
[tool.pytest.ini_options]
testpaths = ["tests"]
实战:配置 PyImage Split 项目
让我们以 PyImage Split 项目为例,这是一个使用 PySide6 构建的图片查看和拆分工具。
项目结构
pyimage_split/
├── pyproject.toml # 项目配置文件
├── README.md # 项目说明
├── src/ # 源代码目录
│ └── pyimage_split/ # 主包
│ ├── __init__.py # 包初始化
│ └── main.py # 主程序
└── tests/ # 测试目录
└── test_main.py # 单元测试
完整的 pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pyimage-split"
version = "0.1.0"
description = "图片查看和均匀拆分工具"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"PySide6>=6.5.0",
"Pillow>=9.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=23.0",
"ruff>=0.1.0",
]
[project.scripts]
pyimage-split = "pyimage_split.main:main"
[project.urls]
Homepage = "https://github.com/yourusername/pyimage-split"
Documentation = "https://github.com/yourusername/pyimage-split#readme"
Repository = "https://github.com/yourusername/pyimage-split"
[tool.setuptools.packages.find]
where = ["src"]
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "I"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
各配置部分详解
1. [build-system] - 构建系统配置
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
这是 pyproject.toml 中必须包含的部分,由 PEP 518 定义。
| 字段 | 说明 |
|---|---|
requires | 构建项目所需的依赖包列表 |
build-backend | 指定构建后端 |
常用构建后端对比:
| 构建后端 | 特点 | 适用场景 |
|---|---|---|
setuptools.build_meta | 功能全面,兼容性好 | 通用项目 |
poetry.core.masonry.api | Poetry 生态 | 使用 Poetry 管理的项目 |
flit_core.buildapi | 简单轻量 | 纯 Python 包 |
hatchling | 现代化,功能丰富 | 新项目推荐 |
pdm.backend | PDM 生态 | 使用 PDM 管理的项目 |
2. [project] - 项目元数据
这是项目的核心配置部分,由 PEP 621 定义。
基本信息
[project]
name = "pyimage-split"
version = "0.1.0"
description = "图片查看和均匀拆分工具"
| 字段 | 必需 | 说明 |
|---|---|---|
name | 是 | 项目名称,用于 pip 安装 |
version | 是 | 版本号,遵循 SemVer 规范 |
description | 否 | 简短描述 |
项目命名规范:
- 使用小写字母和连字符(如
pyimage-split) - 包名使用下划线(如
pyimage_split) - 避免与已有 PyPI 包重名
README 和许可证
readme = "README.md"
license = {text = "MIT"}
readme 支持多种格式:
- 字符串:
readme = "README.md" - 指定类型:
readme = {file = "README.rst", content-type = "text/x-rst"}
license 的写法:
- 简单文本:
license = {text = "MIT"} - 引用文件:
license = {file = "LICENSE"}
作者信息
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
maintainers = [
{name = "Maintainer Name", email = "maintainer@example.com"}
]
Python 版本要求
requires-python = ">=3.8"
常见写法:
>=3.8:3.8 及以上>=3.8,<4.0:3.8 到 3.x~=3.8:兼容 3.8.x
分类器 (Classifiers)
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
分类器用于在 PyPI 上对项目进行分类,完整列表见:https://pypi.org/classifiers/
常用分类器:
| 类别 | 示例 |
|---|---|
| 开发状态 | Development Status :: 3 - Alpha |
| 目标用户 | Intended Audience :: Developers |
| 许可证 | License :: OSI Approved :: MIT License |
| 操作系统 | Operating System :: OS Independent |
| 编程语言 | Programming Language :: Python :: 3 |
| 主题 | Topic :: Software Development :: Libraries |
3. dependencies - 项目依赖
dependencies = [
"PySide6>=6.5.0",
"Pillow>=9.0.0",
]
版本约束语法:
| 语法 | 含义 | 示例 |
|---|---|---|
>= | 大于等于 | requests>=2.28.0 |
<= | 小于等于 | requests<=3.0.0 |
== | 精确版本 | requests==2.28.1 |
!= | 排除版本 | requests!=2.28.0 |
~= | 兼容版本 | requests~=2.28.0 (等于 >=2.28.0,<2.29.0) |
* | 通配符 | requests==2.28.* |
组合使用:
dependencies = [
"requests>=2.28.0,<3.0.0",
"numpy>=1.20.0; python_version>='3.9'", # 条件依赖
]
4. [project.optional-dependencies] - 可选依赖
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=23.0",
"ruff>=0.1.0",
]
docs = [
"sphinx>=5.0",
"sphinx-rtd-theme>=1.0",
]
all = [
"pyimage-split[dev,docs]",
]
安装方式:
# 安装开发依赖
pip install -e ".[dev]"
# 安装文档依赖
pip install -e ".[docs]"
# 安装所有可选依赖
pip install -e ".[all]"
5. [project.scripts] - 命令行入口点
[project.scripts]
pyimage-split = "pyimage_split.main:main"
这会创建一个名为 pyimage-split 的命令,执行时调用 pyimage_split.main 模块的 main 函数。
格式解析:
命令名 = "包名.模块名:函数名"
安装后可直接在命令行使用:
$ pyimage-split
其他入口点类型:
# GUI 脚本(Windows 下无控制台窗口)
[project.gui-scripts]
pyimage-split-gui = "pyimage_split.main:main"
# 插件入口点
[project.entry-points."myapp.plugins"]
plugin1 = "mypackage.plugins:Plugin1"
6. [project.urls] - 项目链接
[project.urls]
Homepage = "https://github.com/yourusername/pyimage-split"
Documentation = "https://github.com/yourusername/pyimage-split#readme"
Repository = "https://github.com/yourusername/pyimage-split"
Changelog = "https://github.com/yourusername/pyimage-split/blob/main/CHANGELOG.md"
"Bug Tracker" = "https://github.com/yourusername/pyimage-split/issues"
这些链接会显示在 PyPI 项目页面上。
常用工具配置
[tool.setuptools] - Setuptools 配置
[tool.setuptools.packages.find]
where = ["src"]
include = ["pyimage_split*"]
exclude = ["tests*"]
使用 src 布局的项目需要指定 where = ["src"]。
动态版本号:
如果想从 __init__.py 读取版本号:
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "pyimage_split.__version__"}
[tool.black] - 代码格式化
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
| 配置项 | 说明 | 默认值 |
|---|---|---|
line-length | 每行最大字符数 | 88 |
target-version | 目标 Python 版本 | 自动检测 |
include | 包含的文件模式 | \.pyi?$ |
exclude | 排除的文件模式 | 常见缓存目录 |
[tool.ruff] - 代码检查
Ruff 是一个用 Rust 编写的快速 Python linter,可以替代 flake8、isort 等多个工具。
[tool.ruff]
line-length = 88
select = [
"E", # pycodestyle errors
"F", # pyflakes
"W", # pycodestyle warnings
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by black)
]
[tool.ruff.isort]
known-first-party = ["pyimage_split"]
[tool.pytest.ini_options] - 测试配置
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "-v --tb=short"
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
| 配置项 | 说明 |
|---|---|
testpaths | 测试文件目录 |
python_files | 测试文件名模式 |
addopts | 默认命令行参数 |
markers | 自定义标记 |
[tool.mypy] - 类型检查
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "tests.*"
ignore_errors = true
[tool.coverage] - 代码覆盖率
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["tests/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
show_missing = true
项目安装与分发
本地开发安装
# 进入项目目录
cd pyimage_split
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/macOS
# 或
venv\Scripts\activate # Windows
# 可编辑模式安装(推荐开发时使用)
pip install -e .
# 安装开发依赖
pip install -e ".[dev]"
-e 参数表示可编辑模式(editable mode),修改源码后无需重新安装。
构建分发包
# 安装构建工具
pip install build
# 构建
python -m build
构建完成后,dist/ 目录下会生成:
pyimage_split-0.1.0.tar.gz:源码分发包 (sdist)pyimage_split-0.1.0-py3-none-any.whl:构建分发包 (wheel)
发布到 PyPI
# 安装 twine
pip install twine
# 检查分发包
twine check dist/*
# 上传到 TestPyPI(测试)
twine upload --repository testpypi dist/*
# 上传到 PyPI(正式)
twine upload dist/*
最佳实践与常见问题
最佳实践
-
使用 src 布局
project/ ├── src/ │ └── package_name/ ├── tests/ └── pyproject.toml优点:避免直接导入源码目录,确保测试的是安装后的包。
-
版本号管理
- 遵循语义化版本 (SemVer):
主版本.次版本.修订号 - 考虑使用动态版本或
bump2version工具
- 遵循语义化版本 (SemVer):
-
依赖版本约束
- 运行依赖:使用宽松约束
>=1.0.0 - 开发依赖:可以使用更严格约束
- 运行依赖:使用宽松约束
-
分离可选依赖
[project.optional-dependencies] dev = ["pytest", "black"] docs = ["sphinx"] -
配置所有工具
将 black、ruff、pytest、mypy 等配置都放入pyproject.toml
常见问题
Q1: 如何从 setup.py 迁移?
可以使用 ini2toml 工具自动转换:
pip install ini2toml[full]
ini2toml setup.cfg > pyproject.toml
Q2: 如何处理动态内容?
[project]
dynamic = ["version", "readme"]
[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}
readme = {file = ["README.md", "CHANGELOG.md"]}
Q3: 如何添加数据文件?
[tool.setuptools.package-data]
mypackage = ["*.txt", "data/*.json"]
或使用 MANIFEST.in 文件。
Q4: 为什么安装后找不到命令?
检查以下几点:
- 虚拟环境是否激活
[project.scripts]配置是否正确- 入口函数是否存在
Q5: 如何支持多个 Python 版本?
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
总结
pyproject.toml 是现代 Python 项目的标准配置方式,它带来了以下好处:
- 统一性:一个文件管理所有配置
- 可读性:TOML 格式清晰易懂
- 安全性:声明式配置,无代码执行
- 兼容性:所有现代工具都支持
通过本文的 PyImage Split 项目示例,我们详细介绍了:
[build-system]:构建系统配置[project]:项目元数据[project.optional-dependencies]:可选依赖分组[project.scripts]:命令行入口点[tool.*]:各种开发工具配置
建议新项目直接使用 pyproject.toml,老项目也可以逐步迁移。这不仅能让你的项目更加规范,也能更好地与 Python 生态系统中的各种工具协作。
参考资料
- PEP 518 - Specifying Minimum Build System Requirements
- PEP 621 - Storing project metadata in pyproject.toml
- Python Packaging User Guide
- Setuptools Documentation
- TOML 规范
本文以 PyImage Split 项目为例,该项目是一个使用 PySide6 构建的图片查看和拆分工具,完整代码可在项目仓库中查看。
312

被折叠的 条评论
为什么被折叠?



