Python的PEP 517/518构建标准:现代打包系统的后端实现与交互
大家好!今天我们来深入探讨Python的PEP 517/518构建标准,这是一个现代Python打包系统的基石。我们将从原理、实践到应用,逐步拆解这个强大的规范,并通过代码示例演示如何实现和使用它。
1. 为什么要引入PEP 517/518?
在PEP 517/518出现之前,Python的打包过程高度依赖setuptools。虽然setuptools功能强大,但它也存在一些问题:
- 侵入性:
setup.py文件通常需要导入setuptools,这使得构建过程与setuptools紧密耦合,即使项目本身并不需要setuptools的所有功能。 - 版本冲突: 不同项目可能需要不同版本的
setuptools,这会导致依赖冲突。 - 标准化程度低: 构建过程的细节很大程度上由
setuptools控制,缺乏统一的标准。
PEP 517/518旨在解决这些问题,通过引入明确的接口和标准化的流程,将构建过程与setuptools解耦,允许使用不同的构建后端,并提供更灵活的打包方式。
2. PEP 517的核心思想:构建后端
PEP 517的核心是构建后端(build backend)。构建后端是一个Python模块,它定义了一组函数,用于执行构建过程的各个阶段。 这些函数包括:
get_requires_for_build_wheel(config_settings=None):返回构建Wheel文件所需的依赖项列表。prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):生成Wheel文件的元数据(.dist-info目录)。build_wheel(wheel_directory, config_settings=None, metadata_directory=None):构建Wheel文件。get_requires_for_build_sdist(config_settings=None):返回构建Source Distribution (sdist)所需的依赖项列表。prepare_metadata_for_build_sdist(metadata_directory, config_settings=None):生成Source Distribution的元数据。build_sdist(sdist_directory, config_settings=None):构建Source Distribution。
这些函数构成了构建后端的API。构建前端(例如pip)会调用这些函数来执行构建过程。
3. PEP 518:pyproject.toml文件
PEP 518引入了pyproject.toml文件,用于声明项目的构建系统信息。该文件必须位于项目根目录下,并包含[build-system]部分。 [build-system]部分包含以下键:
requires:构建后端所需的依赖项列表。build-backend:构建后端的模块名称。backend-path(可选):构建后端模块所在的目录列表。
例如,一个简单的pyproject.toml文件可能如下所示:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
这个文件声明使用setuptools.build_meta作为构建后端,并且需要setuptools和wheel作为构建依赖项。
4. 构建流程详解
构建流程可以分为以下几个步骤:
- 读取
pyproject.toml: 构建前端(例如pip)首先读取pyproject.toml文件,获取构建系统信息。 - 安装构建依赖项: 根据
pyproject.toml中的requires列表,安装构建后端所需的依赖项。 - 加载构建后端: 构建前端根据
pyproject.toml中的build-backend和backend-path,加载构建后端模块。 - 调用构建后端函数: 构建前端根据需要,调用构建后端的各个函数,例如
get_requires_for_build_wheel、build_wheel等,来执行构建过程。
5. 实现一个简单的构建后端
为了更好地理解PEP 517/518,我们来实现一个简单的构建后端。这个后端将非常简单,只用于构建Wheel文件,并且不依赖任何外部库。
首先,创建一个项目目录,例如myproject。
mkdir myproject
cd myproject
然后,创建以下文件:
-
pyproject.toml:[build-system] requires = [] build-backend = "my_build_backend" -
my_build_backend.py:import os import shutil import sys import toml def get_requires_for_build_wheel(config_settings=None): """Return a list of dependencies for building a wheel.""" return [] def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): """Generate the metadata for the wheel.""" distinfo_dir = os.path.join(metadata_directory, "myproject-0.1.0.dist-info") os.makedirs(distinfo_dir, exist_ok=True) # Create METADATA file metadata_path = os.path.join(distinfo_dir, "METADATA") with open(metadata_path, "w") as f: f.write( """Metadata-Version: 2.1 Name: myproject Version: 0.1.0 Summary: A simple project Author: Your Name License: MIT """ ) # Create top-level.txt file toplevel_path = os.path.join(distinfo_dir, "top_level.txt") with open(toplevel_path, "w") as f: f.write("myprojectn") return "myproject-0.1.0.dist-info" def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): """Build the wheel.""" # Create the package directory package_dir = os.path.join("myproject") os.makedirs(package_dir, exist_ok=True) with open(os.path.join(package_dir, "__init__.py"), "w") as f: f.write("") # Create an empty __init__.py # Prepare metadata if metadata_directory is None: metadata_directory = "myproject.dist-info" # Assuming prepare_metadata created this prepare_metadata_for_build_wheel(metadata_directory) distinfo_dir = os.path.join(metadata_directory, "myproject-0.1.0.dist-info") # Create the wheel filename wheel_name = "myproject-0.1.0-py3-none-any.whl" wheel_path = os.path.join(wheel_directory, wheel_name) # Use zipfile to create the wheel archive import zipfile with zipfile.ZipFile(wheel_path, "w", zipfile.ZIP_DEFLATED) as wheel_file: # Add the package directory for root, _, files in os.walk("myproject"): for file in files: file_path = os.path.join(root, file) archive_path = os.path.relpath(file_path, ".") # Path inside the archive wheel_file.write(file_path, archive_path) # Add the metadata for root, _, files in os.walk(distinfo_dir): for file in files: file_path = os.path.join(root, file) archive_path = os.path.relpath(file_path, metadata_directory + '/myproject-0.1.0.dist-info') wheel_file.write(file_path, archive_path) # Clean up the temporary metadata directory # shutil.rmtree(metadata_directory) # Commented out for debugging return wheel_name -
myproject/__init__.py(创建一个空的包)# This file is intentionally left blank.
这个构建后端非常简单,它手动创建Wheel文件的元数据和包目录,并将它们打包成一个zip文件。
现在,我们可以使用pip来构建Wheel文件:
python -m pip wheel . --no-cache-dir
--no-cache-dir 选项确保pip不会使用缓存,而是每次都重新构建。
执行完命令后,应该会在当前目录下看到一个dist目录,其中包含构建好的Wheel文件。
6. 使用hatchling作为构建后端
hatchling是一个现代化的、易于使用的构建后端。它提供了许多便利的功能,例如自动生成元数据、支持插件等。
要使用hatchling,首先需要安装它:
pip install hatchling
然后,修改pyproject.toml文件:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myproject"
version = "0.1.0"
description = "A simple project"
authors = [
{ name = "Your Name", email = "[email protected]" }
]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[tool.hatch.build.targets.wheel]
packages = ["myproject"]
在这个pyproject.toml文件中,我们指定hatchling.build作为构建后端,并使用[project]部分来定义项目的元数据。[tool.hatch.build.targets.wheel]部分指定了要包含在Wheel文件中的包。
创建一个 README.md 文件:
# My Project
A simple project.
现在,我们可以使用pip来构建Wheel文件:
python -m pip wheel . --no-cache-dir
hatchling会自动根据pyproject.toml文件中的信息生成元数据和Wheel文件。
7. 使用flit作为构建后端
flit是另一个流行的构建后端,它专注于简单性和易用性。flit的设计目标是尽可能减少配置,并提供开箱即用的功能。
要使用flit,首先需要安装它:
pip install flit
然后,修改pyproject.toml文件:
[build-system]
requires = ["flit_core >=3.4"]
build-backend = "flit_core.buildapi"
[project]
name = "myproject"
version = "0.1.0"
description = "A simple project"
authors = [{name = "Your Name", email = "[email protected]"}]
license = "MIT"
readme = "README.md"
requires-python = ">=3.6"
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
]
[tool.flit.ini_file]
create_module = true
在这个pyproject.toml文件中,我们指定flit_core.buildapi作为构建后端,并使用[project]部分来定义项目的元数据。[tool.flit.ini_file]部分指定了是否自动创建模块。
创建一个 README.md 文件:
# My Project
A simple project.
现在,我们可以使用pip来构建Wheel文件:
python -m pip wheel . --no-cache-dir
flit会自动根据pyproject.toml文件中的信息生成元数据和Wheel文件。
8. 构建后端的选择
选择合适的构建后端取决于项目的需求和偏好。以下是一些常用的构建后端及其特点:
| 构建后端 | 特点 | 适用场景 |
|---|---|---|
setuptools |
功能强大,历史悠久,社区支持广泛。 | 适用于需要高度定制化构建过程的项目,或者已经使用setuptools的项目。 |
hatchling |
现代化,易于使用,支持插件,自动生成元数据。 | 适用于希望使用现代化的构建工具,并且需要灵活的配置和插件支持的项目。 |
flit |
简单易用,配置少,开箱即用。 | 适用于希望快速构建简单项目,并且不需要复杂的配置的项目。 |
poetry-core |
是Poetry的构建后端,集成依赖管理,打包,发布等功能。需要Poetry作为依赖管理器。 | 适用于使用Poetry作为依赖管理器的项目。 |
| 自定义 | 可以完全控制构建过程,灵活性最高。 | 适用于需要非常特殊的构建过程,或者需要与现有系统集成,并且没有现成的构建后端满足需求的,较为复杂的项目。 |
9. 构建配置
构建后端通常允许通过config_settings参数来配置构建过程。config_settings是一个字典,可以包含各种配置选项。 例如,setuptools允许通过config_settings来指定构建Wheel文件的标签。
构建前端可以通过命令行参数或配置文件来设置config_settings。例如,使用pip时,可以使用--config-settings选项:
python -m pip wheel . --no-cache-dir --config-settings=--global-option=--verbose
10. 构建钩子
一些构建后端(例如hatchling)提供了构建钩子,允许在构建过程的特定阶段执行自定义代码。构建钩子可以用于执行各种任务,例如代码生成、测试等。
11. 构建标准带来的好处
PEP 517/518构建标准带来了许多好处:
- 解耦: 将构建过程与
setuptools解耦,允许使用不同的构建后端。 - 标准化: 提供标准化的构建流程,使得构建过程更加可预测和可重复。
- 灵活性: 允许使用不同的构建后端,并可以通过
config_settings来配置构建过程。 - 可维护性: 使得项目更容易维护和升级。
12. 实际应用案例
PEP 517/518已经在许多流行的Python项目中得到应用。例如,requests、numpy、scipy等项目都使用pyproject.toml文件来声明构建系统信息。
13. 总结:更灵活、更标准、更现代的打包方式
总的来说,PEP 517/518构建标准为Python打包系统带来了革命性的变化,它使得构建过程更加灵活、标准化和可维护,允许开发者选择最适合自己项目的构建后端,并可以通过配置和钩子来定制构建过程。掌握PEP 517/518对于现代Python开发者来说至关重要。
更多IT精英技术系列讲座,到智猿学院