Notice: While Javascript is not essential for this website, your interaction with the content will be limited. Please turn Javascript on for the full experience.

Dependency specification in pyproject.toml based on PEP 508

PEP:631
Title:Dependency specification in pyproject.toml based on PEP 508
Author:Ofek Lev <ofekmeister at gmail.com>
Sponsor:Paul Ganssle <paul at ganssle.io>
Discussions-To:https://discuss.python.org/t/5018
Status:Draft
Type:Standards Track
Created:20-Aug-2020
Post-History:20-Aug-2020

Abstract

This PEP specifies how to write a project's dependencies in a pyproject.toml file for packaging-related tools to consume using the fields defined in PEP 621 [1].

Entries

All dependency entries MUST be valid PEP 508 strings [2].

Build backends SHOULD abort at load time for any parsing errors.

from packaging.requirements import InvalidRequirement, Requirement

...

try:
    Requirement(entry)
except InvalidRequirement:
    # exit

Specification

dependencies

  • Format: array of strings or inline tables
  • Related core metadata:

Every element SHOULD be an entry.

[project]
dependencies = [
  'PyYAML ~= 5.0',
  'requests[security] < 3',
  'subprocess32; python_version < "3.2"',
]

Future versions of this specification that allow for any element to be an inline table should be considered backwards compatible, and consumers of the [project] table should take this into consideration.

Backends MUST error when encountering these unless the specification is extended; other parsers, particularly those not intended to be listed in a project's build-system.requires, SHOULD consider the entire enclosing field as dynamic [4].

optional-dependencies

Each key is the name of the provided option, with each value being the same type as the dependencies field i.e. an array of strings or inline tables.

[project.optional-dependencies]
tests = [
  'coverage>=5.0.3',
  'pytest',
  'pytest-benchmark[histogram]>=3.2.1',
]

Example

This is a real-world example port of what docker-compose [6] defines.

[project]
dependencies = [
  'cached-property >= 1.2.0, < 2',
  'distro >= 1.5.0, < 2',
  'docker[ssh] >= 4.2.2, < 5',
  'dockerpty >= 0.4.1, < 1',
  'docopt >= 0.6.1, < 1',
  'jsonschema >= 2.5.1, < 4',
  'PyYAML >= 3.10, < 6',
  'python-dotenv >= 0.13.0, < 1',
  'requests >= 2.20.0, < 3',
  'texttable >= 0.9.0, < 2',
  'websocket-client >= 0.32.0, < 1',

  # Conditional
  'backports.shutil_get_terminal_size == 1.0.0; python_version < "3.3"',
  'backports.ssl_match_hostname >= 3.5, < 4; python_version < "3.5"',
  'colorama >= 0.4, < 1; sys_platform == "win32"',
  'enum34 >= 1.0.4, < 2; python_version < "3.4"',
  'ipaddress >= 1.0.16, < 2; python_version < "3.3"',
  'subprocess32 >= 3.5.4, < 4; python_version < "3.2"',
]

[project.optional-dependencies]
socks = [ 'PySocks >= 1.5.6, != 1.5.7, < 2' ]
tests = [
  'ddt >= 1.2.2, < 2',
  'pytest < 6',
  'mock >= 1.0.1, < 4; python_version < "3.4"',
]

Implementation

Parsing

from packaging.requirements import InvalidRequirement, Requirement

def parse_dependencies(config):
    dependencies = config.get('dependencies', [])
    if not isinstance(dependencies, list):
        raise TypeError('Field `project.dependencies` must be an array')

    for i, entry in enumerate(dependencies, 1):
        if not isinstance(entry, str):
            raise TypeError(f'Dependency #{i} of field `project.dependencies` must be a string')

        try:
            Requirement(entry)
        except InvalidRequirement as e:
            raise ValueError(f'Dependency #{i} of field `project.dependencies` is invalid: {e}')

    return dependencies

def parse_optional_dependencies(config):
    optional_dependencies = config.get('optional-dependencies', {})
    if not isinstance(optional_dependencies, dict):
        raise TypeError('Field `project.optional-dependencies` must be a table')

    optional_dependency_entries = {}

    for option, dependencies in optional_dependencies.items():
        if not isinstance(dependencies, list):
            raise TypeError(
                f'Dependencies for option `{option}` of field '
                '`project.optional-dependencies` must be an array'
            )

        entries = []

        for i, entry in enumerate(dependencies, 1):
            if not isinstance(entry, str):
                raise TypeError(
                    f'Dependency #{i} of option `{option}` of field '
                    '`project.optional-dependencies` must be a string'
                )

            try:
                Requirement(entry)
            except InvalidRequirement as e:
                raise ValueError(
                    f'Dependency #{i} of option `{option}` of field '
                    f'`project.optional-dependencies` is invalid: {e}'
                )
            else:
                entries.append(entry)

        optional_dependency_entries[option] = entries

    return optional_dependency_entries

Metadata

def construct_metadata_file(metadata_object):
    """
    https://packaging.python.org/specifications/core-metadata/
    """
    metadata_file = 'Metadata-Version: 2.1\n'

    ...

    if metadata_object.dependencies:
        # Sort dependencies to ensure reproducible builds
        for dependency in sorted(metadata_object.dependencies):
            metadata_file += f'Requires-Dist: {dependency}\n'

    if metadata_object.optional_dependencies:
        # Sort extras and dependencies to ensure reproducible builds
        for option, dependencies in sorted(metadata_object.optional_dependencies.items()):
            metadata_file += f'Provides-Extra: {option}\n'
            for dependency in sorted(dependencies):
                if ';' in dependency:
                    metadata_file += f'Requires-Dist: {dependency} and extra == "{option}"\n'
                else:
                    metadata_file += f'Requires-Dist: {dependency}; extra == "{option}"\n'

    ...

    return metadata_file
Source: https://github.com/python/peps/blob/master/pep-0631.rst