如何将基于 setup.py 的项目现代化?

这里说的项目现代化指的是:

把一个使用传统 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 会自动启用 构建隔离:

  1. 创建一个临时的、干净的虚拟环境(和你当前的 Python 环境完全隔离)。
  2. 只安装 pyproject.toml 中 [build-system].requires 声明的依赖,比如:
python 复制代码
[build-system]
requires = ["setuptools", "wheel", "toml"]
build-backend = "setuptools.build_meta"
  1. 在这个干净环境中运行构建(比如执行 setup.py)。
  2. 构建完成后,自动删除这个临时环境。

这样,无论谁构建你的项目,只要 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 中的项目元数据规范

Setuptools 官方文档 -- Build System Support

How to modernize a setup.py based project?

相关推荐
AI_567810 小时前
Selenium+Python可通过 元素定位→操作模拟→断言验证 三步实现Web自动化测试
服务器·人工智能·python
蒜香拿铁10 小时前
【第三章】python算数运算符
python
52Hz11812 小时前
力扣73.矩阵置零、54.螺旋矩阵、48.旋转图像
python·算法·leetcode·矩阵
weixin_4624462312 小时前
Python 使用 openpyxl 从 URL 读取 Excel 并获取 Sheet 及单元格样式信息
python·excel·openpyxl
毕设源码-钟学长13 小时前
【开题答辩全过程】以 基于Python的健康食谱规划系统的设计与实现为例,包含答辩的问题和答案
开发语言·python
百***787514 小时前
Grok-4.1技术深度解析:双版本架构突破与Python API快速集成指南
大数据·python·架构
2501_9421917714 小时前
基于YOLO11-HSFPN的数字检测与识别模型实现详解
python
忧郁的橙子.15 小时前
26期_01_Pyhton基本语法
python
sunfove15 小时前
实战篇:用 Python 徒手实现模拟退火算法解决 TSP 问题
开发语言·python·模拟退火算法