Python的PEP 517/518构建标准:现代打包系统的后端实现与交互

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作为构建后端,并且需要setuptoolswheel作为构建依赖项。

4. 构建流程详解

构建流程可以分为以下几个步骤:

  1. 读取pyproject.toml 构建前端(例如pip)首先读取pyproject.toml文件,获取构建系统信息。
  2. 安装构建依赖项: 根据pyproject.toml中的requires列表,安装构建后端所需的依赖项。
  3. 加载构建后端: 构建前端根据pyproject.toml中的build-backendbackend-path,加载构建后端模块。
  4. 调用构建后端函数: 构建前端根据需要,调用构建后端的各个函数,例如get_requires_for_build_wheelbuild_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项目中得到应用。例如,requestsnumpyscipy等项目都使用pyproject.toml文件来声明构建系统信息。

13. 总结:更灵活、更标准、更现代的打包方式

总的来说,PEP 517/518构建标准为Python打包系统带来了革命性的变化,它使得构建过程更加灵活、标准化和可维护,允许开发者选择最适合自己项目的构建后端,并可以通过配置和钩子来定制构建过程。掌握PEP 517/518对于现代Python开发者来说至关重要。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注