Differences between revisions 1 and 6 (spanning 5 versions)
Revision 1 as of 2022-02-20 16:34:51
Size: 3832
Comment:
Revision 6 as of 2024-10-28 16:30:57
Size: 7635
Comment: Kill one more line
Deletions are marked like this. Additions are marked like this.
Line 3: Line 3:
The future of Python builds, distribution, and packaging lies in '''pyproject.toml'''. The protocol has been incrementally built through PEPs [[https://www.python.org/dev/peps/pep-0517/|517]] and [[https://www.python.org/dev/peps/pep-0518/|518]] (a pair), [[https://www.python.org/dev/peps/pep-0621/|PEP 621]], [[https://www.python.org/dev/peps/pep-0631/|PEP 631]], and [[https://www.python.org/dev/peps/pep-0660/|PEP 660]]. The Python package build system is split into frontends and backends according to protocols found in several PEPs; chiefly [[https://www.python.org/dev/peps/pep-0517/|517]] and [[https://www.python.org/dev/peps/pep-0660/|PEP 660]]. In modern usage, this system is orchestrated through the '''`pyproject.toml`''' file.
Line 11: Line 11:
== Build System ==

This is the focus of PEP 517. A minimal example is:
== Legacy ==

The original packaging system depended on specially-crafted `setup.py` scripts.

{{{
#!/usr/bin/env python3
from setuptools import setup
from pathlib import Path

this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()

setup(
  name='my-project',
  version=1.0.2,
  description='This is the short description',
  long_description=long_description,
  long_description_content_type='text/markdown',
  license='GPL',
  author='John Doe',
  author_email='[email protected]',
  url='example.com/my-project',
  install_requires=[
      'toml>=0.10.2',
  ],
  entry_points={
    'console_scripts': [
      'my-project-cli = my-project:main',
    ],
  },
)
}}}

To pull the version from a source control mechanism, snippets like this were used:

{{{
import subprocess

ver = subprocess.run(['git', 'describe', '--tags'], stdout=subprocess.PIPE).stdout.decode().strip()

setup(
  ...
  version=ver,
  ...
)
}}}

To reduce the size of built packages, source code files to be included were sometimes dynamically selected with snippets like:

{{{
from glob import glob

setup(
  ...
  package_data={
    'my-project': [
      'Makefile',
      'README.md',
    ] + [f[5:] for f in glob('static/**', recursive=True)]
  },
  ...
)
}}}

----



== PEPs 517, 660, and 518 ==

[[https://peps.python.org/pep-0518/|PEP 518]] introduced the '''`pyproject.toml`''' file, intended to declare a package's build backend.

To continue using the legacy build system (i.e., `setup.py`), the following was sufficient.
Line 21: Line 91:
----



== Project ==

This is the focus of PEP 621. It is more-or-less a direct mapping of a boilerplate `setup.py` file into the TOML format.

{{{
[project]
name = "my-project"
New packaging tools were encouraged to adopt this file specification and read from separately-named tables. (Note: in the TOML specification, `[table-name]` declares a table and any lines following it are attributes to that table.)



=== Setup.cfg ===

While `pyproject.toml` was adopted, `setuptools` did not embrace it. As a half-way step, a declarative configuration language was introduced for the '''`setup.cfg`''' file.

A minimal `setup.cfg` looked like:

{{{
[metadata]
name = my-project
version = 1.0.2
description = This is the short description
long_description = file: path/to/my/long/description
license = GPL
author = John Doe
author_email = [email protected]
url = example.com/my-project

[options]
packages = my-project
python_requires = >= 3.6
install_requires =
    toml >= 0.10.2

[options.entry_points]
console_scripts =
    my-project-cli = my-project:main

[options.package_data]
my-project =
    Makefile
    README.md
    static/*
}}}

This enabled the use of minimal shim `setup.py` files:

{{{
#!/usr/bin/env python
import setuptools
if __name__ == "__main__":
    setuptools.setup()
}}}

With this pair of files, `setup.py install` remained a functional build command.

Note that the ability to include globbing patterns under `[options.package_data]` was added much later. Previously, the recommendation was to ''not'' migrate off of `setup.py`.

----



== PEPs 631 and 621 ==

[[https://peps.python.org/pep-0631/|PEP 631]] designed the '''`[project]`''' table for `pyproject.toml`.

{{{
[project]
name = "my-project"
Line 33: Line 154:
readme = "path/to/my/long/description"                     
version = "1.0.2"                                                     
authors = [ { name = "John Doe", email = "[email protected]" } ] 
urls = { homepage = "example.com/my-project" }                   
license = { file = "path/to/my/license" }          
requires-python = ">=3.6"                
dependencies = [                         
    "toml >= 0.10.1",                    
readme = "path/to/my/long/description"
version = "1.0.2"
authors = [ { name = "John Doe", email = "[email protected]" } ]
urls = { homepage = "example.com/my-project" }
license = { file = "path/to/my/license" }
requires-python = ">=3.6"
dependencies = [
    "toml >= 0.10.1",
Line 47: Line 168:
Some notable changes from `setup.py`:
Note the similarities to the `setup.cfg` syntax. Some notable changes to highlight:
Line 63: Line 183:
This PEP was ultimately superseded by the specification of [[https://peps.python.org/pep-0621/|PEP 621]].
Line 67: Line 189:
PEP 631 furthered the design of `dependencies` and introduced `optional-dependencies`. This is the `pyproject.toml` of `docker-compose`: The dependencies attribute can be heavily customized. As an example, this was the `pyproject.toml` of `docker-compose` at one point:
Line 102: Line 224:
=== Metadata ===

Prior to the acceptance of PEP 621, some projects developed a hack to centralize their build information in `pyproject.toml`:

 * insert a `[metadata]` table
 * in `setup.py`, import a TOML parser and parse `pyproject.toml`
 * set all metadata based on the parsed values

While this may still be encountered in the wild, it should not be replicated.
Note that the syntax for version specification is inherited from [[https://peps.python.org/pep-0440/#version-specifiers|PEP 440]].



=== Package Data ===

`setuptools` continues to support limiting package data. This functionality is moved to a separately-named table.

{{{
[tool.setuptools.package-data]
my-project = ["Makefile", "README.md", "static/*"]
}}}

Note that globbing is now supported. It is necessary to use Unix-style paths for all glob patterns, even when building on Windows.



=== Setuptools SCM ===

`setuptools_scm` is an extension of `setuptools` for pulling version information dynamically from source control mechanisms.

First, `version` '''must''' be removed from the `[package]` table and declared as dynamic.

{{{
[project]
# version = "1.0.2"
dynamic = ["version"]
}}}

A minimal template for `pyproject.toml` becomes:

{{{
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
# presence enables setuptools-scm
}}}

Note that another feature of `setuptools_scm` is automatic Package Data determination. Built packages only include files tracked by the source control mechanism. To disable this feature, you must disable the Package Data functionality entirely.

{{{
[tool.setuptools]
include-package-data = false
}}}

PyProject

The Python package build system is split into frontends and backends according to protocols found in several PEPs; chiefly 517 and PEP 660. In modern usage, this system is orchestrated through the pyproject.toml file.


Legacy

The original packaging system depended on specially-crafted setup.py scripts.

from setuptools import setup
from pathlib import Path

this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()

setup(
  name='my-project',
  version=1.0.2,
  description='This is the short description',
  long_description=long_description,
  long_description_content_type='text/markdown',
  license='GPL',
  author='John Doe',
  author_email='[email protected]',
  url='example.com/my-project',
  install_requires=[
      'toml>=0.10.2',
  ],
  entry_points={
    'console_scripts': [
      'my-project-cli = my-project:main',
    ],
  },
)

To pull the version from a source control mechanism, snippets like this were used:

import subprocess

ver = subprocess.run(['git', 'describe', '--tags'], stdout=subprocess.PIPE).stdout.decode().strip()

setup(
  ...
  version=ver,
  ...
)

To reduce the size of built packages, source code files to be included were sometimes dynamically selected with snippets like:

from glob import glob

setup(
  ...
  package_data={
    'my-project': [
      'Makefile',
      'README.md',
    ] + [f[5:] for f in glob('static/**', recursive=True)]
  },
  ...
)


PEPs 517, 660, and 518

PEP 518 introduced the pyproject.toml file, intended to declare a package's build backend.

To continue using the legacy build system (i.e., setup.py), the following was sufficient.

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

New packaging tools were encouraged to adopt this file specification and read from separately-named tables. (Note: in the TOML specification, [table-name] declares a table and any lines following it are attributes to that table.)

Setup.cfg

While pyproject.toml was adopted, setuptools did not embrace it. As a half-way step, a declarative configuration language was introduced for the setup.cfg file.

A minimal setup.cfg looked like:

[metadata]
name = my-project
version = 1.0.2
description = This is the short description
long_description = file: path/to/my/long/description
license = GPL
author = John Doe
author_email = [email protected]
url = example.com/my-project

[options]
packages = my-project
python_requires = >= 3.6
install_requires =
    toml >= 0.10.2

[options.entry_points]
console_scripts =
    my-project-cli = my-project:main

[options.package_data]
my-project =
    Makefile
    README.md
    static/*

This enabled the use of minimal shim setup.py files:

import setuptools
if __name__ == "__main__":
    setuptools.setup()

With this pair of files, setup.py install remained a functional build command.

Note that the ability to include globbing patterns under [options.package_data] was added much later. Previously, the recommendation was to not migrate off of setup.py.


PEPs 631 and 621

PEP 631 designed the [project] table for pyproject.toml.

[project]
name = "my-project"
description = "This is the short description"
readme = "path/to/my/long/description"
version = "1.0.2"
authors = [ { name = "John Doe", email = "[email protected]" } ]
urls = { homepage = "example.com/my-project" }
license = { file = "path/to/my/license" }
requires-python = ">=3.6"
dependencies = [
    "toml >= 0.10.1",
]

[project.scripts]
my-project-cli = "my-project:main"

Note the similarities to the setup.cfg syntax. Some notable changes to highlight:

  • long_description

    • a long standing practice was to read a project's README file and provide this as a long_description

    • now, just provide a path to the README file

  • author, author_email, maintainer, maintainer_email

    • authors and maintainers have replaced these

    • this design greatly simplifies the specification of multiple individuals
  • license

    • while a license may continue to be provided as a string, the syntax has changed (license = { text = "GPL" })

    • the current recommendation is to provide a path to the license file
    • future PEPs may build on this design
  • entry_points

    • project.scripts exists as a more-or-less perfect mapping

    • an analogous [projects.gui-scripts] exists for scripts that should only be called in a graphical setting

This PEP was ultimately superseded by the specification of PEP 621.

Dependencies

The dependencies attribute can be heavily customized. As an example, this was the pyproject.toml of docker-compose at one point:

[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"',
]

Note that the syntax for version specification is inherited from PEP 440.

Package Data

setuptools continues to support limiting package data. This functionality is moved to a separately-named table.

[tool.setuptools.package-data]
my-project = ["Makefile", "README.md", "static/*"]

Note that globbing is now supported. It is necessary to use Unix-style paths for all glob patterns, even when building on Windows.

Setuptools SCM

setuptools_scm is an extension of setuptools for pulling version information dynamically from source control mechanisms.

First, version must be removed from the [package] table and declared as dynamic.

[project]
# version = "1.0.2"
dynamic = ["version"]

A minimal template for pyproject.toml becomes:

[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
# presence enables setuptools-scm

Note that another feature of setuptools_scm is automatic Package Data determination. Built packages only include files tracked by the source control mechanism. To disable this feature, you must disable the Package Data functionality entirely.

[tool.setuptools]
include-package-data = false


CategoryRicottone

Python/PyProject (last edited 2024-10-28 17:32:05 by DominicRicottone)