Skip to content

Commit

Permalink
Show 'closest' tag
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jan 12, 2025
1 parent c904720 commit d577fa4
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 37 deletions.
57 changes: 56 additions & 1 deletion crates/uv-distribution-types/src/prioritized_distribution.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use std::fmt::{Display, Formatter};
use std::str::FromStr;

use arcstr::ArcStr;
use owo_colors::OwoColorize;
Expand All @@ -8,7 +9,7 @@ use tracing::debug;
use uv_distribution_filename::{BuildTag, WheelFilename};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{AbiTag, IncompatibleTag, TagPriority, Tags};
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, TagPriority, Tags};
use uv_pypi_types::{HashDigest, Yanked};

use crate::{
Expand Down Expand Up @@ -555,6 +556,60 @@ impl PrioritizedDist {
}
candidates
}

/// Return the "closest" tag for the distribution that is compatible with the given tags.
///
/// The tag must match at least two of the three components (Python, ABI, platform) in order to
/// be considered compatible. If multiple tags are compatible, the tag with the highest priority
/// is returned. If multiple tags have the same priority, the tag with the highest version is
/// returned.
///
/// For example, if `cp313-cp313-macosx_10_9_x86_64` is compatible (but not available), then
/// `cp313-cp313-manylinux2010_x86_64` could be returned.
pub fn closest_tag(&self, tags: &Tags) -> Option<(LanguageTag, AbiTag, String)> {
let mut closest: Option<(TagPriority, LanguageTag, AbiTag, &str)> = None;
for (py, abi, platform, priority) in tags.iter() {
// If this tag is lower-priority than the current closest tag, skip it.
if let Some((existing_priority, ..)) = closest {
if priority < existing_priority {
continue;
}
}
let Ok(py) = LanguageTag::from_str(py) else {
continue;
};
let Ok(abi) = AbiTag::from_str(abi) else {
continue;
};
for (wheel, _) in &self.0.wheels {
for wheel_py in &wheel.filename.python_tag {
let Ok(wheel_py) = LanguageTag::from_str(wheel_py) else {
continue;
};
for wheel_abi in &wheel.filename.abi_tag {
let Ok(wheel_abi) = AbiTag::from_str(wheel_abi) else {
continue;
};
for wheel_platform in &wheel.filename.platform_tag {
// Exactly two of the three components must match.
let compatibility = u8::from(py == wheel_py)
+ u8::from(abi == wheel_abi)
+ u8::from(platform == wheel_platform);
if compatibility == 2 {
let candidate =
(priority, wheel_py, wheel_abi, wheel_platform.as_str());
if Some(candidate) > closest {
closest = Some(candidate);
}
}
}
}
}
}
}

closest.map(|(.., py, abi, platform)| (py, abi, platform.to_string()))
}
}

impl<'a> CompatibleDist<'a> {
Expand Down
16 changes: 16 additions & 0 deletions crates/uv-platform-tags/src/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,22 @@ impl Tags {
.map(|abis| abis.contains_key(abi_tag))
.unwrap_or(false)
}

/// Returns an iterator over all (Python, ABI, platform, priority) tuples.
pub fn iter(&self) -> impl Iterator<Item = (&str, &str, &str, TagPriority)> + '_ {
self.map.iter().flat_map(|(python_tag, abi_tags)| {
abi_tags.iter().flat_map(move |(abi_tag, platform_tags)| {
platform_tags.iter().map(move |(platform_tag, priority)| {
(
python_tag.as_str(),
abi_tag.as_str(),
platform_tag.as_str(),
*priority,
)
})
})
})
}
}

/// The priority of a platform tag.
Expand Down
123 changes: 90 additions & 33 deletions crates/uv-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ impl PubGrubReportFormatter<'_> {
package: package.clone(),
version: candidate.version().clone(),
tags,
closest: self.tags.and_then(|tags| prioritized.closest_tag(tags)),
})
}
}
Expand All @@ -791,6 +792,7 @@ impl PubGrubReportFormatter<'_> {
package: package.clone(),
version: candidate.version().clone(),
tags,
closest: self.tags.and_then(|tags| prioritized.closest_tag(tags)),
})
}
}
Expand All @@ -817,6 +819,7 @@ impl PubGrubReportFormatter<'_> {
package: package.clone(),
version: candidate.version().clone(),
tags,
closest: self.tags.and_then(|tags| prioritized.closest_tag(tags)),
})
}
}
Expand Down Expand Up @@ -1130,6 +1133,8 @@ pub(crate) enum PubGrubHint {
version: Version,
// excluded from `PartialEq` and `Hash`
tags: BTreeSet<LanguageTag>,
// excluded from `PartialEq` and `Hash`
closest: Option<(LanguageTag, AbiTag, String)>,
},
/// No wheels are available for a package, and using source distributions was disabled.
AbiTags {
Expand All @@ -1138,6 +1143,8 @@ pub(crate) enum PubGrubHint {
version: Version,
// excluded from `PartialEq` and `Hash`
tags: BTreeSet<AbiTag>,
// excluded from `PartialEq` and `Hash`
closest: Option<(LanguageTag, AbiTag, String)>,
},
/// No wheels are available for a package, and using source distributions was disabled.
PlatformTags {
Expand All @@ -1146,6 +1153,8 @@ pub(crate) enum PubGrubHint {
version: Version,
// excluded from `PartialEq` and `Hash`
tags: Vec<String>,
// excluded from `PartialEq` and `Hash`
closest: Option<(LanguageTag, AbiTag, String)>,
},
}

Expand Down Expand Up @@ -1587,55 +1596,103 @@ impl std::fmt::Display for PubGrubHint {
package,
version,
tags,
closest,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following Python tag{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
if let Some((py, abi, platform)) = closest {
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following Python tag{s}: {}. The closest match is `{}`.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
format!("{py}-{abi}-{platform}").cyan(),
)
} else {
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following Python tag{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
}
}
Self::AbiTags {
package,
version,
tags,
closest,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following ABI tag{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
if let Some((py, abi, platform)) = closest {
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following ABI tag{s}: {}. The closest match is `{}`.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
format!("{py}-{abi}-{platform}").cyan(),
)
} else {
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following ABI tag{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
}
}
Self::PlatformTags {
package,
version,
tags,
closest,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
write!(
f,
"{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
if let Some((py, abi, platform)) = closest {
write!(
f,
"{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}. The closest match is `{}`.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
format!("{py}-{abi}-{platform}").cyan(),
)
} else {
write!(
f,
"{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/it/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13949,7 +13949,7 @@ fn invalid_platform() -> Result<()> {
hint: Wheels are available for `open3d` (v0.15.2) with the following ABI tags: `cp36m`, `cp37m`, `cp38`, `cp39`
hint: Wheels are available for `open3d` (v0.18.0) on the following platforms: `macosx_11_0_x86_64`, `macosx_13_0_arm64`, `manylinux_2_27_aarch64`, `manylinux_2_27_x86_64`, `win_amd64`
hint: Wheels are available for `open3d` (v0.18.0) on the following platforms: `macosx_11_0_x86_64`, `macosx_13_0_arm64`, `manylinux_2_27_aarch64`, `manylinux_2_27_x86_64`, `win_amd64`. The closest match is `cp310-cp310-win_amd64`.
"###);

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/it/pip_install_scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4132,7 +4132,7 @@ fn no_sdist_no_wheels_with_matching_platform() {
╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching platform tag (e.g., `manylinux_2_17_x86_64`), we can conclude that all versions of package-a cannot be used.
And because you require package-a, we can conclude that your requirements are unsatisfiable.
hint: Wheels are available for `package-a` (v1.0.0) on the following platform: `macosx_10_0_ppc64`
hint: Wheels are available for `package-a` (v1.0.0) on the following platform: `macosx_10_0_ppc64`. The closest match is `py3-none-macosx_10_0_ppc64`.
"###);

assert_not_installed(
Expand Down Expand Up @@ -4175,7 +4175,7 @@ fn no_sdist_no_wheels_with_matching_python() {
╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python implementation tag (e.g., `cp38`), we can conclude that all versions of package-a cannot be used.
And because you require package-a, we can conclude that your requirements are unsatisfiable.
hint: Wheels are available for `package-a` (v1.0.0) with the following Python tag: `graalpy310`
hint: Wheels are available for `package-a` (v1.0.0) with the following Python tag: `graalpy310`. The closest match is `graalpy310-none-any`.
"###);

assert_not_installed(
Expand Down

0 comments on commit d577fa4

Please sign in to comment.