PEP-517 Builder#

Ever wanted to create a package builder like setuptools?

Thanks to PEP-517 and PEP-518 you can create your very own package builders, and make them compatible with pip install path/to/repository, pip wheel, and all sorts of other tools.

wheelfile makes developing your own builder & hooks straightforward.

The code at the bottom of this page shows an example of a simple builder that can create bdist and sdist distributions from a project tree with source code inside src/ directory.

How to use this example#

Package builders are expected to be installed inside the environment site - the project tree is not included in the import search path when looking for the hooks. In order to make this example easier to follow, its code has been packaged into pep-517-example package, and uploaded to PyPI, so that you can install it and fiddle with mechanics right away, without having to create your own package.

You can install this example using:

pip install pep-517-example

Inside this package, there is a simple entry point script that opens the builder source inside an editor. You can run it using:

python -m pep_517_example

Project configuration: pyproject.toml#

PEP-518 and the example builder expect the project tree to include a pyproject.toml file. This file specifies the builder that should be used for creating the package (and installing it afterwards, if pip install is used). Additionally, the builder reads its own configuration from this file.

The builder reads the file using get_config() function.

Here is an example of the contents of this file:

[build-system]
requires = ['pep-517-example']
build-backend = 'pep_517_example.builder'

[tool.pep_517_example]
name = "my_package"
version = "1.0.0"
maintainers = "Jack Sparrow"
maintainers_emails = "sparrow.jack@pearl.black"

Project tree & building a wheel using pip#

Another file that our builder will expect, is requirements.txt. To sum up, a project directory that this builder expects should have the following structure:

.
├── pyproject.toml
├── requirements.txt
└── src
    ├── ...
    └── app.py

After navigating to this directory, you can use the following pip command to build a wheel:

pip wheel . --no-build-isolation

This command will make pip politely run the builder hooks over the project directory tree.

The --no-build-isolation flag will make pip use the builder installed within your environment (the pep_517_example one), instead of downloading it with all of its dependencies from scratch.

After issuing the command above, you should see a wheel po up in the directory you’re currently in:

my_package-1.0.0-py3-none-any.whl

Builder source code#

import toml
import tarfile
from pathlib import Path
from wheelfile import WheelFile


def get_config():
    """Read pyproject.toml"""
    project_config = toml.load('pyproject.toml')
    config = project_config['tool']['pep_517_example']
    return config


# See https://www.python.org/dev/peps/pep-0517/#get-requires-for-build-wheel
def get_requires_for_build_wheel(config_settings):
    return []


# See https://www.python.org/dev/peps/pep-0517/#build-sdist
def build_sdist(sdist_directory, config_settings=None):
    config = get_config()
    distname = config['name']
    version = config['version']

    with tarfile.open(
        f'{distname}-{version}.tar.gz', 'w:gz', format=tarfile.PAX_FORMAT
    ) as sdist:
        sdist.add('./')


# See https://www.python.org/dev/peps/pep-0517/#build-wheel
def build_wheel(wheel_directory,
                config_settings=None, metadata_directory=None):
    config = get_config()

    maintainers = config['maintainers']
    if isinstance(maintainers, list):
        maintainers = ', '.join(maintainers)

    maintainers_emails = config['maintainers_emails']
    if isinstance(maintainers_emails, list):
        maintainers_emails = ', '.join(config['maintainers_emails'])

    requirements = Path('requirements.txt').read_text().splitlines()

    spec = {
        'distname': config['name'],
        'version': config['version'],
    }

    with WheelFile(wheel_directory, 'w', **spec) as wf:
        wf.metadata.maintainer = maintainers
        wf.metadata.maintainer_email = maintainers_emails
        wf.metadata.requires_dists = requirements

        wf.write('src/')

    return wf.filename  # 🧀