Two years since PEP 665 was rejected and three years since I started working towards some lock file solution, I present my next (and last regardless of outcome) attempt at coming up with a lock file standard.
Terms
- “platform”: OS plus CPU
- “environment”: interpreter plus platform
- “distribution”: in the PyPA spec sense, i.e. a project
- “lock entry”: a set of distribution files locked for a specific environment
- “lock file”: a set of lock entries for a specific set of dependency specifiers from a specific set of indexes
Goals
- An environment lock file standard for PyPA specifications - Python Packaging User Guide (i.e. I’m not trying to accommodate conda and this is not what
Poetry.lock
provides which you could consider a boundary/constraint file) - The ability to lock for multiple environments simultaneously for the same set of dependency specifiers
- You can update all entries in a lock file regardless of the platform you are running on (i.e. all inputs into the resolver are recorded in the lock file)
- You can have different lock files for different reasons
- Installation involves determining which lock entry best applies to an environment, then its a linear install of all distribution files in the lock entry, i.e. no evaluation as to whether an individual distribution file should be installed once a lock entry is chosen, so no SAT solver is necessary
- The file format is human-readable for ease of e.g. auditing diffs, but is machine-writable (i.e. not meant to be written by hand)
- Not meant to introduce new packaging concepts outside of lock files themselves
- Make the sdist people happy so this gets accepted
Spec
Lock files should be written out to a pylock.*.toml
file w/ a label to help identify the lock file’s purpose. The file format is TOML.
The allowed keys in the file are listed below (all keys are required unless specified as optional). A TOML file with this also written out can be found at mousebender/pylock.spec.toml at resolve · brettcannon/mousebender · GitHub . Examples of lock files are listed later on.
Anything w/ a means it’s a contentious key.
meta
Metadata version of the file; initially “1.0”. Since this file is designed to be human-readable but machine-writable, versioning the metadata makes sense as we don’t need to keep a backwards-compatible format for humans to directly work with and instead need a way to help tools migrate to newer metadata versions.
indexes
An array of URLs to package indexes to use to find distributions. Recorded in most- to least-preferred order. Recording the indexes used helps when adding new lock entries by making the potential distributions consistent.
dependencies
Array of top-level dependency specifiers. This acts as the input of what to resolve for, so all details are to be included (e.g. extras, markers, etc.).
[[lock]]
A lock entry for an environment.
lock.markers
A table of environment markers used to produce the lock entry.
GitHub - brettcannon/mousebender at resolve-markers-tags-requirements is an alternative that uses a list of relevant markers.
lock.tags
An array of wheel tags supported by the environment as used to produce the lock entry. The tags are sorted from most- to least-preferred.
GitHub - brettcannon/mousebender at resolve-markers-tags-requirements is an alternative that only lists the required wheel tag sets.
[[lock.wheel]]
A wheel file for the lock entry (optional).
lock.wheel.name
The distribution’s normalized name. You cannot solely rely on the wheel filename to calculate this as the file name may not be a valid .whl
file name due to direct references (technically a tool could download the arbitrary URL and inspect it to determine the wheel file details if one so desired). Having the name as a distinct key also has the benefit that its easier to read than from a wheel file name.
lock.wheel.filename
The file’s name.
lock.wheel.origin
A URL or file path (via file://
) where the wheel that was locked against was found. The location does not need to exist in the future, so this should be treated as only a hint to where to look and/or recording where the wheel file originally came from.
lock.wheel.hashes
A table of file hashes; algorithm:value pairs. This makes sure that one is getting the wheel file that was locked against for reproducibility and security purposes.
lock.wheel.direct
Whether origin
is the direct URL in terms of direct_url.json
.
lock.wheel.requires-python
Python version requirement (optional). If an installer chooses to determine environment compatibility that is not as strict as an exact match of lock.markers
and lock.tags
, knowing the supporting Python versions is important to determine if this wheel file is compatible as this is not necessarily communicated via the wheel file name itself.
lock.wheel.dependencies
A list of normalized distribution names which this distribution depends on (optional). Viewing the overall lock entry as the entire worldview of distributions available, each entry can be just the distribution name (a perk of Python not allowing multiple distribution versions simultaneously). This allows for introspection as to why a distribution is included in the lock entry (i.e. calculate the dependency graph between distributions). Details like extras and markers are not necessary as the resolver has already handled them.
Because this is not required to have a successful install, it is considered optional.
[[lock.sdist]]
A source distribution file for the lock entry (optional).
lock.sdist.name
See lock.wheel.name
.
lock.sdist.filename
See lock.wheel.filename
.
lock.sdist.origin
See lock.wheel.origin
.
lock.sdist.hashes
See lock.wheel.hashes
.
lock.sdist.direct
See lock.wheel.direct
.
lock.sdist.requires-python
See lock.wheel.requires-python
.
lock.sdist.dependencies
See lock.wheel.dependencies
for what is recorded in this array (optional). The contents may come from either:
PKG-INFO
ifMetadata-Version
is 2.2 or higher and the appropriate fields are notDynamic
.- From building the sdist.
- Etc.
[[lock.sdist.build-requires]]
An array of files that can be used or were used to build the sdist based on build-system.requires
from pyproject.toml
(optional). The acceptable keys are wheel
and sdist
and their values match what is acceptable under the same name directly under [[lock]]
. Any future expansion of acceptable distribution types under [[lock]]
will also be supported here.
This effectively makes the table a self-contained lock entry just for this sdist with build-system.requires
providing the value for dependencies
.
It absent, it is at the installer’s discretion how to go about building the sdist (including refusing to).
[[lock.git]]
A Git repository of source code for the lock entry (optional).
lock.git.name
See lock.wheel.name
.
lock.git.repo
A URL to the Git repository; it may be a file://
path.
lock.git.commit
The commit of the repository to use. It should be a specific commit and not a tag or branch as those can change.
lock.git.direct
See lock.wheel.direct
.
lock.git.requires-python
See lock.wheel.requires-python
.
lock.git.dependencies
See lock.wheel.dependencies
for what is recorded in this array (optional). The contents may come from:
pyproject.toml
ifproject.dependencies
(andproject.optional-dependencies
as appropriate) exists and is notdynamic
.- From building the repository based on its
pyproject.toml
file. - Etc.
[[lock.git.build-requires]]
See lock.sdist.build-requires
(optional).
[tool]
Same as pyproject.toml
(optional).
Examples
- A lock file with entries for multiple environments.
- A larger lock file w/ accompanying dependency graph of the lock file.
Proof of Concept
The resolve
branch of my mousebender project has a wheels-only, requires-PyPI-PEP-714-metadata lock generation tool (the restrictions are because I only have so much time and we now have alternative installers showing up, so I don’t need to aim for completeness). You can look at the shell script that I use to generate the examples listed above to see how to play with it. The install
subcommand doesn’t do anything but list out the wheel filenames that would be installed since installation isn’t interesting thanks to the 'installer` project and the only decision at install time is which lock entry to use.
The key point, though, is I was able to write a proof-of-concept that produces and consumes lock files based on this spec.
PEP 665 Comparison
The most obvious difference is the inclusion of sdists from the start. But that was facilitated by making each distribution file type their own concept which is also different. The concept of lock entries is also different.