Always use release-only comparisons for requires-python (#4794)

## Summary

There are a few ideas at play here:

1. pip always strips versions to the release when evaluating against a
`Requires-Python`, so we now do the same. That means, e.g., using
`3.13.0b0` will be accepted by a project with `Requires-Python: >=
3.13`, which does _not_ adhere to PEP 440 semantics but is somewhat
intuitive.
2. Because we know we'll only be evaluating against release-only
versions, we can use different semantics in PubGrub that let us collapse
ranges. For example, `python_version >= '3.10' or python_version <
'3.10'` can be collapsed to the truthy marker.

Closes https://github.com/astral-sh/uv/issues/4714.
Closes https://github.com/astral-sh/uv/issues/4272.
Closes https://github.com/astral-sh/uv/issues/4719.
This commit is contained in:
Charlie Marsh 2024-07-04 16:06:52 -04:00 committed by GitHub
parent 11cb0059c1
commit b588054dfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 401 additions and 171 deletions

View file

@ -1,5 +1,5 @@
use pep440_rs::VersionSpecifiers;
use pep508_rs::{MarkerTree, StringVersion};
use pep440_rs::{Version, VersionSpecifiers};
use pep508_rs::MarkerTree;
use uv_python::{Interpreter, PythonVersion};
use crate::{RequiresPython, RequiresPythonBound};
@ -7,7 +7,7 @@ use crate::{RequiresPython, RequiresPythonBound};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PythonRequirement {
/// The installed version of Python.
installed: StringVersion,
installed: Version,
/// The target version of Python; that is, the version of Python for which we are resolving
/// dependencies. This is typically the same as the installed version, but may be different
/// when specifying an alternate Python version for the resolution.
@ -21,11 +21,10 @@ impl PythonRequirement {
/// [`PythonVersion`].
pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self {
Self {
installed: interpreter.python_full_version().clone(),
target: Some(PythonTarget::Version(StringVersion {
string: python_version.to_string(),
version: python_version.python_full_version(),
})),
installed: interpreter.python_full_version().version.only_release(),
target: Some(PythonTarget::Version(
python_version.python_full_version().only_release(),
)),
}
}
@ -36,7 +35,7 @@ impl PythonRequirement {
requires_python: &RequiresPython,
) -> Self {
Self {
installed: interpreter.python_full_version().clone(),
installed: interpreter.python_full_version().version.only_release(),
target: Some(PythonTarget::RequiresPython(requires_python.clone())),
}
}
@ -44,7 +43,7 @@ impl PythonRequirement {
/// Create a [`PythonRequirement`] to resolve against an [`Interpreter`].
pub fn from_interpreter(interpreter: &Interpreter) -> Self {
Self {
installed: interpreter.python_full_version().clone(),
installed: interpreter.python_full_version().version.only_release(),
target: None,
}
}
@ -63,7 +62,7 @@ impl PythonRequirement {
}
/// Return the installed version of Python.
pub fn installed(&self) -> &StringVersion {
pub fn installed(&self) -> &Version {
&self.installed
}
@ -91,7 +90,7 @@ pub enum PythonTarget {
///
/// The use of a separate enum variant allows us to use a verbatim representation when reporting
/// back to the user.
Version(StringVersion),
Version(Version),
/// The [`PythonTarget`] specifier is a set of version specifiers, as extracted from the
/// `Requires-Python` field in a `pyproject.toml` or `METADATA` file.
RequiresPython(RequiresPython),