1. Python打包工具演进与现状
Python打包工具的发展经历了几个重要阶段。早期的distutils是Python标准库中的打包工具,后来被setuptools取代。setuptools引入了setup.py作为项目配置的核心文件,成为多年来Python打包的事实标准。
随着PEP 517和PEP 518的提出,Python打包进入新时代。这些PEP引入了pyproject.toml作为新的项目配置文件,旨在解决setup.py的一些根本性问题:
- 执行依赖问题:setup.py是Python脚本,执行它需要先安装依赖,而安装依赖又需要执行setup.py,形成鸡生蛋蛋生鸡的问题
- 动态性带来的不确定性:setup.py可以包含任意Python代码,导致构建结果不可预测
- 缺乏标准化:不同项目的setup.py实现差异大,工具难以可靠解析
PEP 621进一步规范了pyproject.toml的内容格式,使其成为声明式项目配置的标准文件。现代Python打包工具如flit、poetry和hatch都原生支持pyproject.toml。
重要提示:从Python 3.10开始,官方文档已明确推荐使用pyproject.toml作为首选配置方式,setup.py仅保留向后兼容性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. pyproject.toml与setup.py功能对比
2.1 核心功能重叠区域
pyproject.toml和setup.py都能定义项目元数据,包括:
- 项目名称、版本、作者等信息
- 项目依赖项
- 包发现规则
- 入口点配置
例如,一个基本的setup.py配置:
python复制from setuptools import setup
setup(
name="myproject",
version="0.1",
packages=["myproject"],
install_requires=["requests>=2.25.0"],
)
对应的pyproject.toml配置:
toml复制[project]
name = "myproject"
version = "0.1"
dependencies = [
"requests>=2.25.0"
]
2.2 pyproject.toml的独特优势
- 声明式配置:TOML格式比Python代码更易于工具解析
- 构建系统要求:可以指定构建工具及其版本
- 更丰富的元数据:支持项目URLs、维护者列表等新字段
- 工具无关性:不绑定特定构建工具
2.3 setup.py的剩余用途
虽然pyproject.toml已成为新标准,但setup.py在以下场景仍有必要:
- 自定义构建步骤:如编译C扩展时的特殊处理
- 动态版本号:从git tag或其它地方获取版本号
- 条件依赖:根据Python版本或系统环境安装不同依赖
3. 现代Python项目的最佳实践
3.1 纯pyproject.toml项目
对于大多数新项目,推荐仅使用pyproject.toml。示例配置:
toml复制[build-system]
requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta"
[project]
name = "myproject"
version = "0.1.0"
authors = [
{name = "John Doe", email = "john@example.com"}
]
description = "My awesome project"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"requests>=2.25.0",
"numpy>=1.21.0"
]
[project.optional-dependencies]
test = ["pytest>=6.0.0"]
dev = ["black", "flake8"]
[project.urls]
Homepage = "https://example.com"
Documentation = "https://docs.example.com"
3.2 混合使用场景
当项目需要setup.py的灵活性时,可以采用混合配置。此时pyproject.toml定义基本元数据,setup.py处理特殊逻辑:
toml复制# pyproject.toml
[build-system]
requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta"
[project]
name = "myproject"
version = "0.1.0"
# 其他标准配置...
python复制# setup.py
import re
from pathlib import Path
from setuptools import setup
def get_version():
# 从git tag动态获取版本号
version_file = Path(__file__).parent / "myproject" / "__init__.py"
content = version_file.read_text()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", content)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
setup(
version=get_version(),
# 其他需要动态设置的参数...
)
4. 迁移策略与常见问题
4.1 从setup.py迁移到pyproject.toml
- 基本元数据迁移:将setup()中的参数转换为pyproject.toml的[project]部分
- 依赖项处理:
- install_requires → project.dependencies
- extras_require → project.optional-dependencies
- 包发现配置:
- packages参数 → 使用[tool.setuptools]或自动发现
- 脚本入口点:
- entry_points → [project.entry-points]
4.2 常见兼容性问题
- 构建工具版本:确保setuptools>=61.0.0以支持最新功能
- 遗留工具兼容性:某些工具(如旧版pip)可能不完全支持PEP 621
- 版本号冲突:避免同时在setup.py和pyproject.toml中定义版本号
- 包发现差异:pyproject.toml默认使用自动包发现,可能与setup.py行为不同
4.3 工具链选择建议
- 简单项目:setuptools + pyproject.toml
- 应用开发:poetry(提供依赖管理和发布工具)
- 库开发:flit(轻量级,适合纯Python包)
- 复杂项目:hatch(功能全面,支持多环境)
5. 实际项目中的决策指南
5.1 何时可以完全移除setup.py
满足以下条件时可以安全删除setup.py:
- 项目不包含自定义构建步骤
- 版本号等元数据都是静态的
- 所有构建工具都支持PEP 621
- 不需要支持非常旧的Python版本(<3.7)
5.2 必须保留setup.py的场景
- C扩展项目:需要自定义编译选项
- 复杂包结构:需要精细控制包包含规则
- 动态元数据:如从外部文件读取版本号
- 向后兼容:需要支持旧版pip(<19.0)
5.3 性能考量
pyproject.toml的构建过程通常更快,因为:
- 不需要执行Python代码
- 工具可以缓存解析结果
- 依赖解析更高效
但在包含setup.py的混合项目中,构建工具需要同时处理两种配置,可能导致轻微性能下降。
6. 生态系统支持现状
截至2023年,主要工具对pyproject.toml的支持情况:
| 工具/平台 | 支持程度 | 备注 |
|---|---|---|
| pip (最新版) | 完全支持 | 推荐使用pip>=21.3 |
| setuptools | 完全支持 | 需要setuptools>=61.0 |
| poetry | 原生支持 | 主要配置方式 |
| flit | 原生支持 | 仅使用pyproject.toml |
| PyPI | 完全支持 | 上传时自动处理 |
| Read the Docs | 支持 | 需要配置python.version |
| GitHub Actions | 支持 | 需要正确设置环境 |
我在多个生产项目中的实际体验是,纯pyproject.toml配置显著降低了维护成本,特别是在依赖管理和跨平台构建方面。唯一遇到的问题是某些内部CI系统尚未更新,仍依赖setup.py的存在。对于这种情况,我保留一个最小化的setup.py作为过渡:
python复制from setuptools import setup
setup()
这种空setup.py不会影响构建过程,但能兼容旧系统。随着工具链更新,可以逐步移除这些兼容层。
