Implement --index-strategy unsafe-best-match (#3138)

## Summary

This index strategy resolves every package to the latest possible
version across indexes. If a version is in multiple indexes, the first
available index is selected.

Implements #3137 

This closely matches pip.

## Test Plan

Good question. I'm hesitant to use my certifi example here, since that
would inevitably break when torch removes this package. Please comment!
This commit is contained in:
Yorick 2024-04-27 03:24:54 +02:00 committed by GitHub
parent a0e7d9fe87
commit 43181f1933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 194 additions and 28 deletions

View file

@ -1,3 +1,4 @@
use itertools::Itertools;
use pubgrub::range::Range;
use tracing::debug;
@ -5,6 +6,7 @@ use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
use pep440_rs::Version;
use pep508_rs::MarkerEnvironment;
use uv_configuration::IndexStrategy;
use uv_normalize::PackageName;
use uv_types::InstalledPackagesProvider;
@ -15,9 +17,11 @@ use crate::version_map::{VersionMap, VersionMapDistHandle};
use crate::{Exclusions, Manifest, Options};
#[derive(Debug, Clone)]
#[allow(clippy::struct_field_names)]
pub(crate) struct CandidateSelector {
resolution_strategy: ResolutionStrategy,
prerelease_strategy: PreReleaseStrategy,
index_strategy: IndexStrategy,
}
impl CandidateSelector {
@ -40,6 +44,7 @@ impl CandidateSelector {
markers,
options.dependency_mode,
),
index_strategy: options.index_strategy,
}
}
@ -54,6 +59,12 @@ impl CandidateSelector {
pub(crate) fn prerelease_strategy(&self) -> &PreReleaseStrategy {
&self.prerelease_strategy
}
#[inline]
#[allow(dead_code)]
pub(crate) fn index_strategy(&self) -> &IndexStrategy {
&self.index_strategy
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@ -202,27 +213,54 @@ impl CandidateSelector {
version_maps: &'a [VersionMap],
) -> Option<Candidate> {
tracing::trace!(
"selecting candidate for package {} with range {:?} with {} remote versions",
package_name,
range,
"selecting candidate for package {package_name} with range {range:?} with {} remote versions",
version_maps.iter().map(VersionMap::len).sum::<usize>(),
);
let highest = self.use_highest_version(package_name);
let allow_prerelease = self.allow_prereleases(package_name);
if highest {
version_maps.iter().find_map(|version_map| {
if self.index_strategy == IndexStrategy::UnsafeBestMatch {
if highest {
Self::select_candidate(
version_map.iter().rev(),
version_maps
.iter()
.map(|version_map| version_map.iter().rev())
.kmerge_by(|(version1, _), (version2, _)| version1 > version2),
package_name,
range,
allow_prerelease,
)
})
} else {
Self::select_candidate(
version_maps
.iter()
.map(VersionMap::iter)
.kmerge_by(|(version1, _), (version2, _)| version1 < version2),
package_name,
range,
allow_prerelease,
)
}
} else {
version_maps.iter().find_map(|version_map| {
Self::select_candidate(version_map.iter(), package_name, range, allow_prerelease)
})
if highest {
version_maps.iter().find_map(|version_map| {
Self::select_candidate(
version_map.iter().rev(),
package_name,
range,
allow_prerelease,
)
})
} else {
version_maps.iter().find_map(|version_map| {
Self::select_candidate(
version_map.iter(),
package_name,
range,
allow_prerelease,
)
})
}
}
}
@ -241,7 +279,7 @@ impl CandidateSelector {
/// Select the first-matching [`Candidate`] from a set of candidate versions and files,
/// preferring wheels over source distributions.
fn select_candidate<'a>(
versions: impl Iterator<Item = (&'a Version, VersionMapDistHandle<'a>)> + ExactSizeIterator,
versions: impl Iterator<Item = (&'a Version, VersionMapDistHandle<'a>)>,
package_name: &'a PackageName,
range: &Range<Version>,
allow_prerelease: AllowPreRelease,
@ -253,8 +291,9 @@ impl CandidateSelector {
}
let mut prerelease = None;
let versions_len = versions.len();
for (step, (version, maybe_dist)) in versions.enumerate() {
let mut steps = 0usize;
for (version, maybe_dist) in versions {
steps += 1;
let candidate = if version.any_prerelease() {
if range.contains(version) {
match allow_prerelease {
@ -267,7 +306,7 @@ impl CandidateSelector {
after {} steps: {:?} version",
package_name,
range,
step,
steps,
version,
);
// If pre-releases are allowed, treat them equivalently
@ -308,7 +347,7 @@ impl CandidateSelector {
after {} steps: {:?} version",
package_name,
range,
step,
steps,
version,
);
Candidate::new(package_name, version, dist)
@ -340,7 +379,7 @@ impl CandidateSelector {
after {} steps",
package_name,
range,
versions_len,
steps,
);
match prerelease {
None => None,

View file

@ -1,3 +1,5 @@
use uv_configuration::IndexStrategy;
use crate::{DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
/// Options for resolving a manifest.
@ -7,6 +9,7 @@ pub struct Options {
pub prerelease_mode: PreReleaseMode,
pub dependency_mode: DependencyMode,
pub exclude_newer: Option<ExcludeNewer>,
pub index_strategy: IndexStrategy,
}
/// Builder for [`Options`].
@ -16,6 +19,7 @@ pub struct OptionsBuilder {
prerelease_mode: PreReleaseMode,
dependency_mode: DependencyMode,
exclude_newer: Option<ExcludeNewer>,
index_strategy: IndexStrategy,
}
impl OptionsBuilder {
@ -52,6 +56,13 @@ impl OptionsBuilder {
self
}
/// Sets the index strategy.
#[must_use]
pub fn index_strategy(mut self, index_strategy: IndexStrategy) -> Self {
self.index_strategy = index_strategy;
self
}
/// Builds the options.
pub fn build(self) -> Options {
Options {
@ -59,6 +70,7 @@ impl OptionsBuilder {
prerelease_mode: self.prerelease_mode,
dependency_mode: self.dependency_mode,
exclude_newer: self.exclude_newer,
index_strategy: self.index_strategy,
}
}
}