这里说的项目现代化指的是:
把一个使用传统 Python 打包方式(依赖 setup.py)的项目,升级为符合当前(2020 年代后期)Python 官方推荐标准的打包和构建方式。
为什么要进行现代化
在早些年(比如 2015--2018 年),Python 项目的打包几乎都靠一个叫 setup.py 的文件。你可能见过这样的命令:
python
python setup.py install
python setup.py sdist bdist_wheel
但这种方式存在几个问题:
- 命令不统一:不同工具行为不一致。
- 依赖管理混乱:构建依赖和运行依赖混在一起。
- 可重复性差:构建过程容易受本地环境影响。
- 安全性风险:setup.py 是可执行的 Python 脚本,可能包含任意代码。
为了解决这些问题,Python 社区推出了标准化方案:
✅ PEP 517 和 PEP 518:定义了通用的构建系统接口
✅ pyproject.toml:作为项目的统一配置入口(自 Python 3.7+ 起被官方推荐)
所以项目现代化简单的来说就是做了下面这些事情:
| 传统做法 | 现代做法 |
|---|---|
只有 setup.py |
添加 pyproject.toml 文件 |
用 python setup.py install 安装 |
改用 pip install . |
构建用 python setup.py sdist |
改用 python -m build |
所有配置写在 setup.py 里 |
静态元数据(如 name、version)移到 pyproject.toml 的 [project] 表中 |
| 构建依赖隐式存在 | 显式声明在 pyproject.toml 的 [build-system].requires 中 |
这并不是说 setup.py 必须立刻删除(很多时候它仍需保留以支持动态逻辑或兼容性),而是让它退居二线,由 pyproject.toml 担任主角。
如果你正在维护一个老项目,逐步迁移到这种新结构是非常值得的!
是否应该添加 pyproject.toml 文件?
强烈建议添加 pyproject.toml 文件。
虽然仅仅创建一个空的 pyproject.toml 文件本身并不会带来太多功能上的变化,但真正关键的是其中的 [build-system] 表(table)。这个表告诉构建工具(如 pip、build 等):你的项目使用什么构建后端(build backend),以及构建时需要哪些依赖。
注:pyproject.toml 的存在会影响 pip 的"构建隔离"(build isolation)行为,详见下文。
是否应该删除 setup.py?
不需要删除 setup.py。
在现代基于 Setuptools 的项目中,setup.py 仍然是合法的配置文件------它本质上是一段用 Python 编写的 Setuptools 配置脚本。
不过,以下命令已被弃用,绝对不应再使用,应改用推荐的替代命令:
| 已弃用的命令 | 推荐替代命令 |
|---|---|
python setup.py install |
python -m pip install . |
python setup.py develop |
python -m pip install --editable . |
python setup.py sdist |
python -m build |
python setup.py bdist_wheel |
python -m build |
更多细节可参考:Is setup.py deprecated?
从哪里开始?
你的项目根目录下必须包含一个 pyproject.toml 文件,并至少包含如下 [build-system] 表:
toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
这是标准化的方式,用于告知构建前端(如 pip、build):本项目使用 Setuptools 作为构建后端。
注意:只要存在 pyproject.toml 文件(哪怕为空),pip 就会默认启用"构建隔离"机制。
如何处理额外的构建时依赖?
如果你的 setup.py 除了依赖 Setuptools 之外,还使用了其他第三方库(非 Python 标准库),那么这些依赖必须列在 pyproject.toml 的 build-system.requires 列表中,以便构建工具知道在构建前需要先安装它们。
例如,假设你的 setup.py 如下:
python
import setuptools
import some_build_toolkit # 来自 `some-build-toolkit` 库
def get_version():
version = some_build_toolkit.compute_version()
return version
setuptools.setup(
name="my-project",
version=get_version(),
)
那么对应的 pyproject.toml 应为(setup.py 无需改动):
python
[build-system]
requires = [
"setuptools",
"some-build-toolkit",
]
build-backend = "setuptools.build_meta"
什么是"构建隔离"(Build Isolation)?
构建前端(如 pip 或 build)通常会创建一个临时的虚拟环境,仅安装 build-system.requires 中声明的构建依赖(及其传递依赖),然后在这个隔离环境中执行构建。
所以构建隔离的目的是:确保你的项目在任何机器上都能以一致、干净的方式被构建,不受本地环境干扰。
🌰 举个例子:没有构建隔离会发生什么?
假设你正在开发一个 Python 包 my-awesome-lib,它的 setup.py 里用到了一个叫 toml 的第三方库来读取配置:
python
# setup.py
import toml # ← 注意:这是第三方库,不是标准库!
def get_version():
data = toml.load("pyproject.toml")
return data["project"]["version"]
setuptools.setup(
name="my-awesome-lib",
version=get_version(),
)
现在,你在自己的电脑上开发,已经全局安装了 toml,所以运行:
python
pip install .
一切正常。
但你的同事小李克隆了代码,在他的电脑上运行同样的命令,却报错:
python
ModuleNotFoundError: No module named 'toml'
为什么?因为他没装 toml!
❌ 问题根源:构建过程依赖了开发者本地环境中的包,而不是明确声明的依赖。
✅ 构建隔离如何解决这个问题?
当你使用支持 PEP 517/518 的现代工具(如 pip 或 build),并且项目中有 pyproject.toml,pip 会自动启用 构建隔离:
- 创建一个临时的、干净的虚拟环境(和你当前的 Python 环境完全隔离)。
- 只安装 pyproject.toml 中 [build-system].requires 声明的依赖,比如:
python
[build-system]
requires = ["setuptools", "wheel", "toml"]
build-backend = "setuptools.build_meta"
- 在这个干净环境中运行构建(比如执行 setup.py)。
- 构建完成后,自动删除这个临时环境。
这样,无论谁构建你的项目,只要 pyproject.toml 写对了,就一定能成功,不会因为"我本地装了某个包而你没装"导致失败。
如何处理打包元数据(packaging metadata)?
所有静态元数据(如项目名称、版本号等)都可以选择性地迁移到 pyproject.toml 的 [project] 表中。
例如,原本的 setup.py:
python
import setuptools
setuptools.setup(
name="my-project",
version="1.2.3",
)
可以完全被以下 pyproject.toml 取代:
toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "my-project"
version = "1.2.3"
完整的 [project] 表字段规范请参考:Declaring project metadata: the [project] table
如何处理动态元数据?
如果某些元数据字段是动态生成的(比如通过函数计算版本号),则必须在 [project] 表中将其列为 dynamic。
例如,原 setup.py 如下:
python
import setuptools
import some_build_toolkit
def get_version():
version = some_build_toolkit.compute_version()
return version
setuptools.setup(
name="my-project",
version=get_version(),
)
可现代化为:
pyproject.toml:
python
[build-system]
requires = [
"setuptools",
"some-build-toolkit",
]
build-backend = "setuptools.build_meta"
[project]
name = "my-project"
dynamic = ["version"]
保留精简版 setup.py:
python
import setuptools
import some_build_toolkit
def get_version():
return some_build_toolkit.compute_version()
setuptools.setup(
version=get_version(),
)
这样,Setuptools 会在构建时调用 setup.py 来填充动态字段。
更多关于 dynamic 字段的说明,请参考官方文档中的 dynamic 章节。
如果某些外部流程强制要求存在 setup.py 怎么办?
例如,某个无法修改的 CI 脚本必须执行 python setup.py --name。
完全可以保留一个最小化的 setup.py 文件,即使所有配置都已迁移到 pyproject.toml。该文件只需包含:
python
import setuptools
setuptools.setup()
这样既能满足兼容性需求,又不影响现代化构建流程。
想深入了解?推荐阅读:
PEP 621 -- pyproject.toml 中的项目元数据规范