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

开源AI·十一月创作之星挑战赛 10w+人浏览 556人参与

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

在这里插入图片描述

前言

如果你曾经接触过 Python 项目,可能对 setup.pysetup.cfgrequirements.txt 这些文件并不陌生。但随着 Python 生态的发展,这种分散的配置方式逐渐暴露出诸多问题:配置分散、格式不统一、工具兼容性差等。

pyproject.toml 的出现正是为了解决这些痛点。它是 Python 项目的现代化配置标准,由 PEP 518、PEP 621 等规范定义,旨在提供一个统一的、声明式的项目配置文件。

本文将以一个实际项目 PyImage Split(图片拆分工具)为例,详细讲解如何从零开始配置 pyproject.toml,让你的 Python 项目更加规范、现代化。


目录

  1. 为什么选择 pyproject.toml
  2. pyproject.toml 的基本结构
  3. 实战:配置 PyImage Split 项目
  4. 各配置部分详解
  5. 常用工具配置
  6. 项目安装与分发
  7. 最佳实践与常见问题
  8. 总结

为什么选择 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 = "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.apiPoetry 生态使用 Poetry 管理的项目
flit_core.buildapi简单轻量纯 Python 包
hatchling现代化,功能丰富新项目推荐
pdm.backendPDM 生态使用 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/*

最佳实践与常见问题

最佳实践

  1. 使用 src 布局

    project/
    ├── src/
    │   └── package_name/
    ├── tests/
    └── pyproject.toml
    

    优点:避免直接导入源码目录,确保测试的是安装后的包。

  2. 版本号管理

    • 遵循语义化版本 (SemVer):主版本.次版本.修订号
    • 考虑使用动态版本或 bump2version 工具
  3. 依赖版本约束

    • 运行依赖:使用宽松约束 >=1.0.0
    • 开发依赖:可以使用更严格约束
  4. 分离可选依赖

    [project.optional-dependencies]
    dev = ["pytest", "black"]
    docs = ["sphinx"]
    
  5. 配置所有工具
    将 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 构建的图片查看和拆分工具,完整代码可在项目仓库中查看。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值