mirror of https://github.com/astral-sh/uv
Document why uv discards upper bounds on `requires-python` (#15927)
We're regularly get questions about this. The DPO thread is the best ressource, but it's also a long read, so I summarized some points for uv's decision. --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
3e6fe1da86
commit
787d035d5e
|
|
@ -156,26 +156,62 @@ Windows, Linux and macOS.
|
||||||
To ensure that a resolution with `requires-python = ">=3.9"` can actually be installed for the
|
To ensure that a resolution with `requires-python = ">=3.9"` can actually be installed for the
|
||||||
included Python versions, uv requires that all dependencies have the same minimum Python version.
|
included Python versions, uv requires that all dependencies have the same minimum Python version.
|
||||||
Package versions that declare a higher minimum Python version, e.g., `requires-python = ">=3.10"`,
|
Package versions that declare a higher minimum Python version, e.g., `requires-python = ">=3.10"`,
|
||||||
are rejected, because a resolution with that version can't be installed on Python 3.9. For
|
are rejected, because a resolution with that version can't be installed on Python 3.9. This ensures
|
||||||
simplicity and forward compatibility, only lower bounds in `requires-python` are respected. For
|
that when you are on an old Python version, you can install old packages, instead of getting newer
|
||||||
example, if a package declares `requires-python = ">=3.8,<4"`, the `<4` marker is not propagated to
|
packages that require newer Python syntax or standard library features.
|
||||||
the entire resolution.
|
|
||||||
|
|
||||||
This default is a problem for packages that use the version-dependent C API of CPython, such as
|
uv ignores upper-bounds on `requires-python`, with special handling for packages with only
|
||||||
numpy. Each numpy release support 4 Python minor versions, e.g., numpy 2.0.0 has wheels for CPython
|
ABI-specific wheels. For example, if a package declares `requires-python = ">=3.8,<4"`, the `<4`
|
||||||
3.9 through 3.12 and declares `requires-python = ">=3.9"`, while numpy 2.1.0 has wheels for CPython
|
part is ignored. There is a detailed discussion with drawbacks and alternatives in
|
||||||
3.10 through 3.13 and declares `requires-python = ">=3.10"`. The means that when we resolve a
|
[#4022](https://github.com/astral-sh/uv/issues/4022) and this
|
||||||
`numpy>=2,<3` requirement in a project with `requires-python = ">=3.9"`, we resolve numpy 2.0.0 and
|
[DPO thread](https://discuss.python.org/t/requires-python-upper-limits/12663), this section
|
||||||
the lockfile doesn't install on Python 3.13 or newer. To alleviate this, whenever we reject a
|
summarizes the aspects most relevant to uv's design.
|
||||||
version due to a too high Python requirement, we fork on that Python version. This behavior is
|
|
||||||
controlled by `--fork-strategy`. In the example case, upon encountering numpy 2.1.0 we fork into
|
For most projects, it's not possible to determine whether they will be compatible with a new version
|
||||||
Python versions `>=3.9,<3.10` and `>=3.10` and resolve two different numpy versions:
|
before it's released, so blocking newer versions in advance would block users from upgrading or
|
||||||
|
testing newer Python versions. The exceptions are packages which use the unstable C ABI or internals
|
||||||
|
of CPython such as its bytecode format.
|
||||||
|
|
||||||
|
Introducing a `requires-python` upper bound to a project that previously wasn't using one will not
|
||||||
|
prevent the project from being used on a too recent Python version. Instead of failing, the resolver
|
||||||
|
will pick an older version without the bound, circumventing the bound.
|
||||||
|
|
||||||
|
For the resolution to be as universally installable as possible, uv ensures that the selected
|
||||||
|
dependency versions are compatible with the `requires-python` range of the project. For example, for
|
||||||
|
a project with `requires-python = ">=3.12"`, uv will not use a dependency version with
|
||||||
|
`requires-python = ">=3.13"`, as otherwise the resolution is not installable on Python 3.12, which
|
||||||
|
the project declares to support. Applying the same logic to upper bounds means that bumping the
|
||||||
|
upper Python version bound on a project makes it compatible with less dependency versions,
|
||||||
|
potentially failing to resolve when no version of a dependency supports the required range. (Bumping
|
||||||
|
the lower Python version bound has the inverse effect, it only increases the set of supported
|
||||||
|
dependency versions.)
|
||||||
|
|
||||||
|
Note that this is different for Conda, as the Conda solver also determines the Python version, so it
|
||||||
|
can choose a lower Python version instead. Conda can also change metadata after a release, so it can
|
||||||
|
update compatibility for a new Python version, while metadata on PyPI cannot be changed once
|
||||||
|
published.
|
||||||
|
|
||||||
|
Ignoring an upper bound is a problem for packages such as numpy which use the version-dependent C
|
||||||
|
API of CPython. As of writing, each numpy release support 4 Python minor versions, e.g., numpy 2.0.0
|
||||||
|
has wheels for CPython 3.9 through 3.12 and declares `requires-python = ">=3.9"`, while numpy 2.1.0
|
||||||
|
has wheels for CPython 3.10 through 3.13 and declares `requires-python = ">=3.10"`. The means that
|
||||||
|
when uv resolves a `numpy>=2,<3` requirement in a project with `requires-python = ">=3.9"`, it
|
||||||
|
selects numpy 2.0.0 and the lockfile doesn't install on Python 3.13 or newer. To alleviate this,
|
||||||
|
whenever uv rejects a version that requires a newer Python version, we fork by splitting the
|
||||||
|
resolution markers on that Python version. This behavior can be controlled by `--fork-strategy`. In
|
||||||
|
the example case, upon encountering numpy 2.1.0 we fork into Python versions `>=3.9,<3.10` and
|
||||||
|
`>=3.10` and resolve two different numpy versions:
|
||||||
|
|
||||||
```
|
```
|
||||||
numpy==2.0.0; python_version >= "3.9" and python_version < "3.10"
|
numpy==2.0.0; python_version >= "3.9" and python_version < "3.10"
|
||||||
numpy==2.1.0; python_version >= "3.10"
|
numpy==2.1.0; python_version >= "3.10"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There's one case where uv does consider the upper bound: When the project uses an upper bound on
|
||||||
|
requires Python, such as `requires-python = "==3.13.*"` for an application that only deploys to
|
||||||
|
Python 3.13. uv prunes wheels from the lockfile that are outside the range (e.g., `cp312` and
|
||||||
|
`cp314`) in a post-processing step, which does not influence the resolution itself.
|
||||||
|
|
||||||
## URL dependencies
|
## URL dependencies
|
||||||
|
|
||||||
In uv, a dependency can either be a registry dependency, a package with a version specifier or the
|
In uv, a dependency can either be a registry dependency, a package with a version specifier or the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue