Fork version selection based on requires-python requirements (#9827)

## Summary

This PR addresses a significant limitation in the resolver whereby we
avoid choosing the latest versions of packages when the user supports a
wider range.

For example, with NumPy, the latest versions only support Python 3.10
and later. If you lock a project with `requires-python = ">=3.8"`, we
pick the last NumPy version that supported Python 3.8, and use that for
_all_ Python versions. So you get `1.24.4` for all versions, rather than
`2.2.0`. And we'll never upgrade you unless you bump your
`requires-python`. (Even worse, those versions don't have wheels for
Python 3.12, etc., so you end up building from source.)

(As-is, this is intentional. We optimize for minimizing the number of
selected versions, and the current logic does that well!)

Instead, we know recognize when a version has an elevated
`requires-python` specifier and fork. This is a new fork point, since we
need to fork once we have the package metadata, as opposed to when we
see the dependencies.

In this iteration, I've made this behavior the default. I'm sort of
undecided on whether I want to push on that... Previously, I'd suggested
making it opt-in via a setting
(https://github.com/astral-sh/uv/pull/8686).

Closes https://github.com/astral-sh/uv/issues/8492.
This commit is contained in:
Charlie Marsh
2024-12-13 15:33:46 -05:00
committed by GitHub
parent dc0525ddd0
commit 0ee21146f4
10 changed files with 940 additions and 203 deletions
+112 -24
View File
@@ -1,8 +1,9 @@
use pubgrub::Range;
use std::cmp::Ordering;
use std::collections::Bound;
use std::ops::Deref;
use pubgrub::Range;
use uv_distribution_filename::WheelFilename;
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
@@ -73,24 +74,43 @@ impl RequiresPython {
}
})?;
// Extract the bounds.
let (lower_bound, upper_bound) = range
.bounding_range()
.map(|(lower_bound, upper_bound)| {
(
LowerBound(lower_bound.cloned()),
UpperBound(upper_bound.cloned()),
)
})
.unwrap_or((LowerBound::default(), UpperBound::default()));
// Convert back to PEP 440 specifiers.
let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter());
Some(Self {
specifiers,
range: RequiresPythonRange(lower_bound, upper_bound),
})
// Extract the bounds.
let range = RequiresPythonRange::from_range(&range);
Some(Self { specifiers, range })
}
/// Split the [`RequiresPython`] at the given version.
///
/// For example, if the current requirement is `>=3.10`, and the split point is `3.11`, then
/// the result will be `>=3.10 and <3.11` and `>=3.11`.
pub fn split(&self, bound: Bound<Version>) -> Option<(Self, Self)> {
let RequiresPythonRange(.., upper) = &self.range;
let upper = Range::from_range_bounds((bound, upper.clone().into()));
let lower = upper.complement();
// Intersect left and right with the existing range.
let lower = lower.intersection(&Range::from(self.range.clone()));
let upper = upper.intersection(&Range::from(self.range.clone()));
if lower.is_empty() || upper.is_empty() {
None
} else {
Some((
Self {
specifiers: VersionSpecifiers::from_release_only_bounds(lower.iter()),
range: RequiresPythonRange::from_range(&lower),
},
Self {
specifiers: VersionSpecifiers::from_release_only_bounds(upper.iter()),
range: RequiresPythonRange::from_range(&upper),
},
))
}
}
/// Narrow the [`RequiresPython`] by computing the intersection with the given range.
@@ -489,14 +509,9 @@ impl serde::Serialize for RequiresPython {
impl<'de> serde::Deserialize<'de> for RequiresPython {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone())
.bounding_range()
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
Ok(Self {
specifiers,
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
})
let range = release_specifiers_to_ranges(specifiers.clone());
let range = RequiresPythonRange::from_range(&range);
Ok(Self { specifiers, range })
}
}
@@ -504,6 +519,15 @@ impl<'de> serde::Deserialize<'de> for RequiresPython {
pub struct RequiresPythonRange(LowerBound, UpperBound);
impl RequiresPythonRange {
/// Initialize a [`RequiresPythonRange`] from a [`Range`].
pub fn from_range(range: &Range<Version>) -> Self {
let (lower, upper) = range
.bounding_range()
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
Self(LowerBound(lower), UpperBound(upper))
}
/// Initialize a [`RequiresPythonRange`] with the given bounds.
pub fn new(lower: LowerBound, upper: UpperBound) -> Self {
Self(lower, upper)
@@ -967,4 +991,68 @@ mod tests {
assert_eq!(requires_python.is_exact_without_patch(), expected);
}
}
#[test]
fn split_version() {
// Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`.
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let (lower, upper) = requires_python
.split(Bound::Excluded(Version::new([3, 12])))
.unwrap();
assert_eq!(
lower,
RequiresPython::from_specifiers(
&VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap()
)
);
assert_eq!(
upper,
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">3.12").unwrap())
);
// Splitting `>=3.10` on `>=3.12` should result in `>=3.10, <3.12` and `>=3.12`.
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let (lower, upper) = requires_python
.split(Bound::Included(Version::new([3, 12])))
.unwrap();
assert_eq!(
lower,
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.10, <3.12").unwrap())
);
assert_eq!(
upper,
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap())
);
// Splitting `>=3.10` on `>=3.9` should return `None`.
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
assert!(requires_python
.split(Bound::Included(Version::new([3, 9])))
.is_none());
// Splitting `>=3.10` on `>=3.10` should return `None`.
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
assert!(requires_python
.split(Bound::Included(Version::new([3, 10])))
.is_none());
// Splitting `>=3.9, <3.13` on `>=3.11` should result in `>=3.9, <3.11` and `>=3.11, <3.13`.
let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let (lower, upper) = requires_python
.split(Bound::Included(Version::new([3, 11])))
.unwrap();
assert_eq!(
lower,
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.9, <3.11").unwrap())
);
assert_eq!(
upper,
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.11, <3.13").unwrap())
);
}
}