PEP: 631 Title: Dependency specification in pyproject.toml based on PEP 508 Author: Ofek Lev Sponsor: Paul Ganssle Discussions-To: https://discuss.python.org/t/5018 Status: Draft Type: Standards Track Content-Type: text/x-rst 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`_. Entries ======= All dependency entries MUST be valid `PEP 508 strings`_. 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: - `Requires-Dist`_ Every element SHOULD be an `entry <#entries>`_. :: [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`_. optional-dependencies --------------------- - Format: table - Related core metadata: - `Provides-Extra`_ - `Requires-Dist`_ Each key is the name of the provided option, with each value being the same type as the `dependencies <#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`_ 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('Dependency #{} of field `project.dependencies` must be a string'.format(i)) try: Requirement(entry) except InvalidRequirement as e: raise ValueError('Dependency #{} of field `project.dependencies` is invalid: {}'.format(i, e)) return sorted(dependencies, key=lambda d: d.lower()) 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 = OrderedDict() for option, dependencies in sorted(optional_dependencies.items()): if not isinstance(dependencies, list): raise TypeError( 'Dependencies for option `{}` of field `project.optional-dependencies` ' 'must be an array'.format(option) ) entries = [] for i, entry in enumerate(dependencies, 1): if not isinstance(entry, str): raise TypeError( 'Dependency #{} of option `{}` of field `project.optional-dependencies` ' 'must be a string'.format(i, option) ) try: Requirement(entry) except InvalidRequirement as e: raise ValueError( 'Dependency #{} of option `{}` of field `project.optional-dependencies` ' 'is invalid: {}'.format(i, option, e) ) else: entries.append(entry) optional_dependency_entries[option] = sorted(entries, key=lambda d: d.lower()) 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: for dependency in metadata_object.dependencies: metadata_file += 'Requires-Dist: {}\n'.format(dependency) if metadata_object.optional_dependencies: for option, dependencies in metadata_object.optional_dependencies.items(): metadata_file += 'Provides-Extra: {}\n'.format(option) for dependency in dependencies: if ';' in dependency: metadata_file += 'Requires-Dist: {} and extra == "{}"\n'.format(dependency, option) else: metadata_file += 'Requires-Dist: {}; extra == "{}"\n'.format(dependency, option) ... return metadata_file Copyright ========= This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive. .. _fields defined in PEP 621: https://www.python.org/dev/peps/pep-0621/#dependencies-optional-dependencies .. _PEP 508 strings: https://www.python.org/dev/peps/pep-0508/ .. _Requires-Dist: https://packaging.python.org/specifications/core-metadata/#requires-dist-multiple-use .. _dynamic: https://www.python.org/dev/peps/pep-0621/#dynamic .. _Provides-Extra: https://packaging.python.org/specifications/core-metadata/#provides-extra-multiple-use .. _docker-compose: https://github.com/docker/compose/blob/789bfb0e8b2e61f15f423d371508b698c64b057f/setup.py#L28-L61 .. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 coding: utf-8 End: