前言

如果你曾经接触过 Python 项目,可能对 setup.pysetup.cfgrequirements.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 配置

这种方式存在以下问题:

  1. 配置分散:相关配置散落在多个文件中,难以维护
  2. 格式不统一:INI、CFG、TXT、Python 脚本混杂
  3. setup.py 的安全隐患:作为 Python 脚本,可能执行任意代码
  4. 依赖管理混乱:运行依赖和开发依赖分离管理

pyproject.toml 的优势

  1. 统一配置:所有配置集中在一个文件
  2. 标准格式:使用 TOML 格式,语法清晰易读
  3. 声明式配置:无需执行代码,更安全
  4. 工具兼容:现代 Python 工具都支持
  5. 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 = "[email protected]"}
]
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 = "[email protected]"}
]
maintainers = [
{name = "Maintainer Name", email = "[email protected]"}
]

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 工具

依赖版本约束

  • 运行依赖:使用宽松约束 >=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: 为什么安装后找不到命令?

检查以下几点:

  1. 虚拟环境是否激活
  2. [project.scripts] 配置是否正确
  3. 入口函数是否存在

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 项目的标准配置方式,它带来了以下好处:

  1. 统一性:一个文件管理所有配置
  2. 可读性:TOML 格式清晰易懂
  3. 安全性:声明式配置,无代码执行
  4. 兼容性:所有现代工具都支持

通过本文的 PyImage Split 项目示例,我们详细介绍了:

  • [build-system]:构建系统配置
  • [project]:项目元数据
  • [project.optional-dependencies]:可选依赖分组
  • [project.scripts]:命令行入口点
  • [tool.*]:各种开发工具配置

建议新项目直接使用 pyproject.toml,老项目也可以逐步迁移。这不仅能让你的项目更加规范,也能更好地与 Python 生态系统中的各种工具协作。

本文以 PyImage Split 项目为例,该项目是一个使用 PySide6 构建的图片查看和拆分工具,完整代码可在项目仓库中查看。

以上就是使用pyproject.toml构建现代化Python项目的详细步骤的详细内容,更多关于pyproject.toml构建Python项目的资料请关注本站其它相关文章!

声明:本站(华域联盟www.cnhackhy.com)所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。