mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Prefer higher Python lower-bounds when forking (#10007)
## Summary With the advent of `--fork-strategy requires-python` (the default), we actually _want_ to solve higher lower-bound forks before lower lower-bound forks. The former ensures we get the most compatible versions, while the latter ensures we get fewer overall versions. These two strategies match up with `--fork-strategy`, but need to be respected as such. Closes https://github.com/astral-sh/uv/issues/9998.
This commit is contained in:
parent
8f88f98350
commit
4d3c1b325e
9 changed files with 487 additions and 134 deletions
|
@ -624,7 +624,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
}
|
||||
ForkedDependencies::Forked {
|
||||
forks,
|
||||
mut forks,
|
||||
diverging_packages,
|
||||
} => {
|
||||
debug!(
|
||||
|
@ -633,6 +633,28 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
// Prioritize the forks.
|
||||
match (self.options.fork_strategy, self.options.resolution_mode) {
|
||||
(ForkStrategy::Fewest, _) | (_, ResolutionMode::Lowest) => {
|
||||
// Prefer solving forks with lower Python bounds, since they're more
|
||||
// likely to produce solutions that work for forks with higher
|
||||
// Python bounds (whereas the inverse is not true).
|
||||
forks.sort_by(|a, b| {
|
||||
a.cmp_requires_python(b)
|
||||
.reverse()
|
||||
.then_with(|| a.cmp_upper_bounds(b))
|
||||
});
|
||||
}
|
||||
(ForkStrategy::RequiresPython, _) => {
|
||||
// Otherwise, prefer solving forks with higher Python bounds, since
|
||||
// we want to prioritize choosing the latest-compatible package
|
||||
// version for each Python version.
|
||||
forks.sort_by(|a, b| {
|
||||
a.cmp_requires_python(b).then_with(|| a.cmp_upper_bounds(b))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for new_fork_state in self.forks_to_fork_states(
|
||||
state,
|
||||
&version,
|
||||
|
@ -2907,11 +2929,6 @@ impl Dependencies {
|
|||
} else if forks.len() == 1 {
|
||||
ForkedDependencies::Unforked(forks.pop().unwrap().dependencies)
|
||||
} else {
|
||||
// Prioritize the forks. Prefer solving forks with lower Python
|
||||
// bounds, since they're more likely to produce solutions that work
|
||||
// for forks with higher Python bounds (whereas the inverse is not
|
||||
// true).
|
||||
forks.sort();
|
||||
ForkedDependencies::Forked {
|
||||
forks,
|
||||
diverging_packages: diverging_packages.into_iter().collect(),
|
||||
|
@ -3224,6 +3241,50 @@ impl Fork {
|
|||
});
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Compare forks, preferring forks with g `requires-python` requirements.
|
||||
fn cmp_requires_python(&self, other: &Self) -> Ordering {
|
||||
// A higher `requires-python` requirement indicates a _higher-priority_ fork.
|
||||
//
|
||||
// This ordering ensures that we prefer choosing the highest version for each fork based on
|
||||
// its `requires-python` requirement.
|
||||
//
|
||||
// The reverse would prefer choosing fewer versions, at the cost of using older package
|
||||
// versions on newer Python versions. For example, if reversed, we'd prefer to solve `<3.7
|
||||
// before solving `>=3.7`, since the resolution produced by the former might work for the
|
||||
// latter, but the inverse is unlikely to be true.
|
||||
let self_bound = self.env.requires_python().unwrap_or_default();
|
||||
let other_bound = other.env.requires_python().unwrap_or_default();
|
||||
self_bound.lower().cmp(other_bound.lower())
|
||||
}
|
||||
|
||||
/// Compare forks, preferring forks with upper bounds.
|
||||
fn cmp_upper_bounds(&self, other: &Self) -> Ordering {
|
||||
// We'd prefer to solve `numpy <= 2` before solving `numpy >= 1`, since the resolution
|
||||
// produced by the former might work for the latter, but the inverse is unlikely to be true
|
||||
// due to maximum version selection. (Selecting `numpy==2.0.0` would satisfy both forks, but
|
||||
// selecting the latest `numpy` would not.)
|
||||
let self_upper_bounds = self
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter(|dep| {
|
||||
dep.version
|
||||
.bounding_range()
|
||||
.is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded))
|
||||
})
|
||||
.count();
|
||||
let other_upper_bounds = other
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter(|dep| {
|
||||
dep.version
|
||||
.bounding_range()
|
||||
.is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded))
|
||||
})
|
||||
.count();
|
||||
|
||||
self_upper_bounds.cmp(&other_upper_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Fork {}
|
||||
|
@ -3234,50 +3295,6 @@ impl PartialEq for Fork {
|
|||
}
|
||||
}
|
||||
|
||||
impl Ord for Fork {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// A higher `requires-python` requirement indicates a _lower-priority_ fork. We'd prefer
|
||||
// to solve `<3.7` before solving `>=3.7`, since the resolution produced by the former might
|
||||
// work for the latter, but the inverse is unlikely to be true.
|
||||
let self_bound = self.env.requires_python().unwrap_or_default();
|
||||
let other_bound = other.env.requires_python().unwrap_or_default();
|
||||
|
||||
other_bound.lower().cmp(self_bound.lower()).then_with(|| {
|
||||
// If there's no difference, prioritize forks with upper bounds. We'd prefer to solve
|
||||
// `numpy <= 2` before solving `numpy >= 1`, since the resolution produced by the former
|
||||
// might work for the latter, but the inverse is unlikely to be true due to maximum
|
||||
// version selection. (Selecting `numpy==2.0.0` would satisfy both forks, but selecting
|
||||
// the latest `numpy` would not.)
|
||||
let self_upper_bounds = self
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter(|dep| {
|
||||
dep.version
|
||||
.bounding_range()
|
||||
.is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded))
|
||||
})
|
||||
.count();
|
||||
let other_upper_bounds = other
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter(|dep| {
|
||||
dep.version
|
||||
.bounding_range()
|
||||
.is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded))
|
||||
})
|
||||
.count();
|
||||
|
||||
self_upper_bounds.cmp(&other_upper_bounds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Fork {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an error if a conflicting extra is found in the given requirements.
|
||||
///
|
||||
/// Specifically, if there is any conflicting extra (just one is enough) that
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue