Update the resolution concept documentation (#5813)

This commit is contained in:
Zanie Blue 2024-08-06 12:06:06 -05:00 committed by GitHub
parent e651e67f29
commit 83d6f59e2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,50 +1,126 @@
# Resolution # Resolution
Dependency resolution is the process of taking your requirements and converting them to a list of Resolution is the process of taking a list of requirements and converting them to a list of package
package versions that fulfil your requirements and the requirements of all included packages. versions that fulfill the requirements. Resolution requires recursively searching for compatible
versions of packages, ensuring that the requested requirements are fulfilled and that the
requirements of the requested packages are compatible.
## Overview ## Dependencies
Imagine you have the following dependency tree: Most projects and packages have dependencies. Dependencies are other packages that are needed in
order for the current package to work. A package defines its dependencies as _requirements_, roughly
a combination of a package name and acceptable versions. The dependencies defined by the current
project are called _direct dependencies_. The requirements added by each dependency of the current
project are called _indirect_ or _transitive dependencies_.
- Your project depends on `foo>=1,<3` and `bar>=1,<3`. !!! note
- `foo` has two versions, 1.0.0 and 2.0.0. `foo` 2.0.0 depends on `lib==2.0.0`, `foo` 1.0.0 has no
dependencies. See the [dependency specifiers
- `bar` has two versions, 1.0.0 and 2.0.0. `bar` 2.0.0 depends on `lib==1.0.0`, `bar` 1.0.0 has no page](https://packaging.python.org/en/latest/specifications/dependency-specifiers/)
dependencies. in the Python Packaging documentation for details about dependencies.
## Basic examples
To help demonstrate the resolution process, consider the following dependencies:
<!-- prettier-ignore -->
- The project depends on `foo` and `bar`.
- `foo` has one version, 1.0.0:
- `foo 1.0.0` depends on `lib>=1.0.0`.
- `bar` has one version, 1.0.0:
- `bar 1.0.0` depends on `lib>=2.0.0`.
- `lib` has two versions, 1.0.0 and 2.0.0. Both versions have no dependencies. - `lib` has two versions, 1.0.0 and 2.0.0. Both versions have no dependencies.
We can't install both `foo` 2.0.0 and `bar` 2.0.0 because they conflict on the version of `lib`, so In this example, the resolver must find a set of package versions which satisfies the project
the resolver will pick either `foo` 1.0.0 or `bar` 1.0.0. Both are valid solutions, at the resolvers requirements. Since there is only one version of both `foo` and `bar`, those will be used. The
choice. resolution must also include the transitive dependencies, so a version of `lib` must be chosen.
`foo 1.0.0` allows all of the available versions of `lib`, but `bar 1.0.0` requires `lib>=2.0.0` so
`lib 2.0.0` must be used.
## Platform-specific and universal resolution In some resolutions, there is more than one solution. Consider the following dependencies:
uv supports two modes of resolution: Platform-specific and universal (platform-independent). <!-- prettier-ignore -->
- The project depends on `foo` and `bar`.
- `foo` has two versions, 1.0.0 and 2.0.0:
- `foo 1.0.0` has no dependencies.
- `foo 2.0.0` depends on `lib==2.0.0`.
- `bar` has two versions, 1.0.0 and 2.0.0:
- `bar 1.0.0` has no dependencies.
- `bar 2.0.0` depends on `lib==1.0.0`
- `lib` has two versions, 1.0.0 and 2.0.0. Both versions have no dependencies.
Like `pip` and `pip-tools`, `uv pip compile` produces a resolution that's only known to be In this example, some version of both `foo` and `bar` must be picked, however, determining which
compatible with the current operating system, architecture, Python version and Python interpreter. version requires considering the dependencies of each version of `foo` and `bar`. `foo 2.0.0` and
`uv pip compile --universal` and the [project](../guides/projects.md) interface on the other hand `bar 2.0.0` cannot be installed together because they conflict on their required version of `lib`,
will solve to a host-agnostic universal resolution that can be used across platforms. so the resolver must select either `foo 1.0.0` or `bar 1.0.0`. Both are valid solutions, and
different resolution algorithms may give either result.
For universal resolution, you need to configure the minimum required python version. For ## Platform markers
`uv pip compile --universal`, you can pass `--python-version`, otherwise the current Python version
Markers allow attaching an expression to requirements that indicate when the dependency should be
used. For example `bar; python_version<"3.9"` can be used to only require `bar` on Python 3.8 and
older.
Markers are used to adjust a package's dependencies dependending on the current environment or
platform. For example, markers can be used to change dependencies based on the operating system, the
CPU architecture, the Python version, the Python implementation, and more.
!!! note
See the [environment
markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers)
section in the Python Packaging documentation for more details about markers.
Markers are important for resolution because their values change the required dependencies.
Typically, Python package resolvers use the markers of the _current_ platform to determine which
dependencies to use since the package is often being _installed_ on the current platform. However,
for _locking_ dependencies this is problematic — the lockfile would only work for developers using
the same platform the lockfile was created on. To solve this problem, platform-independent, or
"universal" resolvers exist.
uv supports both [platform-specific](#platform-specific-resolution) and
[universal](#universal-resolution) resolution.
## Universal resolution
uv's lockfile (`uv.lock`) is created with a universal resolution and is portable across platforms.
This ensures that dependencies are locked for everyone working on the project, regardless of
operating system, architecture, and Python version. The uv lockfile is created and modified by
[project](../concepts/projects.md) commands such as `uv lock`, `uv sync`, and `uv add`.
universal resolution is also available in uv's pip interface, i.e.,
[`uv pip compile`](../pip/compile.md), with the `--universal` flag. The resulting requirements file
will contain markers to indicate which platform each dependency is relevant for.
During universal resolution, a package may be listed multiple times with different versions or URLs
if different versions are needed for different platforms — the markers determine which version will
be used. A universal resolution is often more constrained than a platform-specific resolution, since
we need to take the requirements for all markers into account.
During universal resolution, a minimum Python version must be specified. Project commands read the
minimum required version from `project.requires-python` in the `pyproject.toml`. When using the pip
interface, provide a value with the `--python-version` option, otherwise the current Python version
will be treated as a lower bound. For example, `--universal --python-version 3.9` writes a universal will be treated as a lower bound. For example, `--universal --python-version 3.9` writes a universal
resolution for Python 3.9 and later. Project commands such as `uv sync` or `uv lock` read resolution for Python 3.9 and later.
`project.requires-python` from your `pyproject.toml`.
Setting the minimum Python version is important because all package versions we select have to be Setting the minimum Python version is important because all package versions we select have to be
compatible with the python range. For example, a universal resolution of `numpy<2` with compatible with the Python version range. For example, a universal resolution of `numpy<2` with
`--python-version 3.8` resolves to `numpy==1.24.4`, while `--python-version 3.9` resolves to `--python-version 3.8` resolves to `numpy==1.24.4`, while `--python-version 3.9` resolves to
`numpy==1.26.4`, as `numpy` releases after 1.26.4 require at least Python 3.9. Note that we only `numpy==1.26.4`, as `numpy` releases after 1.26.4 require at Python 3.9+. Note that we only consider
consider the lower bound of any Python requirement. the lower bound of any Python requirement, upper bounds are always ignored.
In platform-specific mode, the `uv pip` interface also supports resolving for specific alternate ## Platform-specific resolution
platforms and Python versions with `--python-platform` and `--python-version`. For example, if
you're running Python 3.12 on macOS, but want to resolve for Linux with Python 3.10, you can run By default, uv's pip interface, i.e., [`uv pip compile`](../pip/compile.md), produces a resolution
`uv pip compile --python-platform linux --python-version 3.10 requirements.in` to produce a that is platform-specific, like `pip-tools`. There is no way to use platform-specific resolution in
`manylinux2014`-compatible resolution. In this mode, `--python-version` is the exact python version the uv's project interface.
to use, not a lower bound.
uv also supports resolving for specific, alternate platforms and Python versions with the
`--python-platform` and `--python-version` options. For example, if using Python 3.12 on macOS,
`uv pip compile --python-platform linux --python-version 3.10 requirements.in` can be used to
produce a resolution for Python 3.10 on Linux instead. Unlike universal resolution, during
platform-specific resolution, the provided `--python-version` is the exact python version to use,
not a lower bound.
!!! note !!! note
@ -55,34 +131,28 @@ to use, not a lower bound.
compatible with any machine running on the target `--python-platform`, which should be sufficient for compatible with any machine running on the target `--python-platform`, which should be sufficient for
most use cases, but may lose fidelity for complex package and platform combinations. most use cases, but may lose fidelity for complex package and platform combinations.
In universal mode, a package may be listed multiple times with different versions or URLs. In this ## Dependency preferences
case, uv determined that we need different versions to be compatible different platforms, and the
markers decides on which platform we use which version. A universal resolution is often more
constrained than a platform-specific resolution, since we need to take the requirements for all
markers into account.
If an output file is used with `uv pip` or `uv.lock` exist with the project commands, we try to If resolution output file exists, i.e. a uv lockfile (`uv.lock`) or a requirements output file
resolve to the versions present there, considering them preferences in the resolution. The same (`requirements.txt`), uv will _prefer_ the dependency versions listed there. Similarly, if
applies to version already installed to the active virtual environments. You can override this with installing a package into a virtual environment, uv will prefer the already installed version if
`--upgrade`. present. This means that locked or installed versions will not change unless an incompatible version
is requested or an upgrade is explicitly requested with `--upgrade`.
## Resolution strategy ## Resolution strategy
By default, uv tries to use the latest version of each package. For example, By default, uv tries to use the latest version of each package. For example,
`uv pip install flask>=2.0.0` will install the latest version of Flask (at time of writing: `uv pip install flask>=2.0.0` will install the latest version of Flask, e.g., 3.0.0. If
`3.0.0`). If you have `flask>=2.0.0` as a dependency of your library, you will only test `flask` `flask>=2.0.0` is a dependency of the project, only `flask` 3.0.0 will be used. This is important,
3.0.0 this way, but not if you are actually still compatible with `flask` 2.0.0. for example, because running tests will not check that the the project is actually compatible with
its stated lower bound of `flask` 2.0.0.
With `--resolution lowest`, uv will install the lowest possible version for all dependencies, both With `--resolution lowest`, uv will install the lowest possible version for all dependencies, both
direct and indirect (transitive). Alternatively, `--resolution lowest-direct` will opt for the direct and indirect (transitive). Alternatively, `--resolution lowest-direct` will use the lowest
lowest compatible versions for all direct dependencies, while using the latest compatible versions compatible versions for all direct dependencies, while using the latest compatible versions for all
for all other dependencies. uv will always use the latest versions for build dependencies. other dependencies. uv will always use the latest versions for build dependencies.
For libraries, we recommend separately running tests with `--resolution lowest` or For example, given the following `requirements.in` file:
`--resolution lowest-direct` in continuous integration to ensure compatibility with the declared
lower bounds.
As an example, given the following `requirements.in` file:
```text title="requirements.in" ```text title="requirements.in"
flask>=2.0.0 flask>=2.0.0
@ -128,6 +198,10 @@ werkzeug==2.0.0
# via flask # via flask
``` ```
When publishing libraries, it is recommended to separately run tests with `--resolution lowest` or
`--resolution lowest-direct` in continuous integration to ensure compatibility with the declared
lower bounds.
## Pre-release handling ## Pre-release handling
By default, uv will accept pre-release versions during dependency resolution in two cases: By default, uv will accept pre-release versions during dependency resolution in two cases:
@ -136,46 +210,53 @@ By default, uv will accept pre-release versions during dependency resolution in
(e.g., `flask>=2.0.0rc1`). (e.g., `flask>=2.0.0rc1`).
1. If _all_ published versions of a package are pre-releases. 1. If _all_ published versions of a package are pre-releases.
If dependency resolution fails due to a transitive pre-release, uv will prompt the user to re-run If dependency resolution fails due to a transitive pre-release, uv will prompt use of
with `--prerelease allow`, to allow pre-releases for all dependencies. `--prerelease allow` to allow pre-releases for all dependencies.
Alternatively, you can add the transitive dependency to your `requirements.in` file with a Alternatively, the transitive dependency can be added as a [constraint](#dependency-constraints) or
pre-release specifier (e.g., `flask>=2.0.0rc1`) to opt in to pre-release support for that specific direct dependency (i.e. in `requirements.in` or `pyproject.toml`) with a pre-release version
dependency. specifier (e.g., `flask>=2.0.0rc1`) to opt-in to pre-release support for that specific dependency.
Pre-releases are Pre-releases are
[notoriously difficult](https://pubgrub-rs-guide.netlify.app/limitations/prerelease_versions) to [notoriously difficult](https://pubgrub-rs-guide.netlify.app/limitations/prerelease_versions) to
model, and are a frequent source of bugs in other packaging tools. uv's pre-release handling is model, and are a frequent source of bugs in other packaging tools. uv's pre-release handling is
_intentionally_ limited and _intentionally_ requires user opt-in for pre-releases, to ensure _intentionally_ limited and requires user opt-in for pre-releases to ensure correctness.
correctness.
For more, see [Pre-release compatibility](../pip/compatibility.md#pre-release-compatibility). For more details, see
[Pre-release compatibility](../pip/compatibility.md#pre-release-compatibility).
## Constraints ## Dependency constraints
Like `pip`, uv supports constraints files (`--constraint constraints.txt`), which allows users to uv supports constraints files (`--constraint constraints.txt`), like pip, which narrow the set of
narrow the set of acceptable versions for a given package. A constraint files is like a regular acceptable versions for the given packages. Constraint files are like a regular requirements files,
requirements files, but it doesn't add packages, it only constrains their version range when they but they do not add packages to the requirements — they only take affect if the package is requested
are depended on by a regular requirement. in a direct or transitive dependency. Constraints are often useful for reducing the range of
available versions for a transitive dependency without adding a direct requirement on the package.
## Overrides ## Dependency overrides
Sometimes, the requirements in one of your (transitive) dependencies are too strict, and you want to Sometimes, the requirements defined by a dependency are too strict, and a working version of a
install a version of a package that you know to work, but wouldn't be allowed regularly. Overrides package is not allowed (often, causing resolution to fail). uv allows overriding requirements
allow you to lie to the resolver, replacing all other requirements for that package with the defined by dependencies to unblock resolution.
override. They break the usual rules of version resolving and should only be used as last resort
measure.
For example, if a transitive dependency declares `pydantic>=1.0,<2.0`, but the user knows that the For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _works_
package is compatible with `pydantic>=2.0`, the user can override the declared dependency with with `pydantic>=2.0`, the user can override the declared dependency with `pydantic>=1.0,<3` to allow
`pydantic>=2.0,<3` to allow the resolver to continue. the resolver to installer a newer version of `pydantic`.
Overrides are passed to `uv pip` as `--override` with an overrides file with the same syntax as Constraints and direct dependency declarations will _not_ result in the same behavior as an
requirements or constraints files. In `pyproject.toml`, you can set `tool.uv.override-dependencies` override, as they can only add _additional_ constraints to the allowed version instead of
to a list of requirements. If you provide multiple overrides for the same package, we apply them _replacing_ existing constraints. As with constraints, overrides do not _add_ a dependency on the
simultaneously, while markers are applied as usual. package and only take affect if the package is requested in a direct or transitive dependency.
## Time-restricted reproducible resolutions In a `pyproject.toml`, use `tool.uv.override-dependencies` to define a list of overrides. In the
pip-compatible interface, the `--override` option can be used to pass files with the same format as
constraints files.
If multiple overrides are provided for the same package, they must be differentiated with
[markers](#platform-markers). If a package has a dependency with a marker, it is replaced
unconditionally when using overrides — it does not matter if the marker evaluates to true or false.
## Reproducible resolutions
uv supports an `--exclude-newer` option to limit resolution to distributions published before a uv supports an `--exclude-newer` option to limit resolution to distributions published before a
specific date, allowing reproduction of installations regardless of new package releases. The date specific date, allowing reproduction of installations regardless of new package releases. The date