From d577fa4a13a67b37869c7b63a8d88a9026d5dac0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 11 Jan 2025 21:41:00 -0500 Subject: [PATCH] Show 'closest' tag --- .../src/prioritized_distribution.rs | 57 +++++++- crates/uv-platform-tags/src/tags.rs | 16 +++ crates/uv-resolver/src/pubgrub/report.rs | 123 +++++++++++++----- crates/uv/tests/it/pip_compile.rs | 2 +- crates/uv/tests/it/pip_install_scenarios.rs | 4 +- 5 files changed, 165 insertions(+), 37 deletions(-) diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index aa432e066376..75c6811052a9 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; use std::fmt::{Display, Formatter}; +use std::str::FromStr; use arcstr::ArcStr; use owo_colors::OwoColorize; @@ -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::{ @@ -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> { diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 3f41222c4939..4513f6e2fd91 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -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 + '_ { + 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. diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index a3c0f22856e5..a120f58e96b2 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -766,6 +766,7 @@ impl PubGrubReportFormatter<'_> { package: package.clone(), version: candidate.version().clone(), tags, + closest: self.tags.and_then(|tags| prioritized.closest_tag(tags)), }) } } @@ -791,6 +792,7 @@ impl PubGrubReportFormatter<'_> { package: package.clone(), version: candidate.version().clone(), tags, + closest: self.tags.and_then(|tags| prioritized.closest_tag(tags)), }) } } @@ -817,6 +819,7 @@ impl PubGrubReportFormatter<'_> { package: package.clone(), version: candidate.version().clone(), tags, + closest: self.tags.and_then(|tags| prioritized.closest_tag(tags)), }) } } @@ -1130,6 +1133,8 @@ pub(crate) enum PubGrubHint { version: Version, // excluded from `PartialEq` and `Hash` tags: BTreeSet, + // excluded from `PartialEq` and `Hash` + closest: Option<(LanguageTag, AbiTag, String)>, }, /// No wheels are available for a package, and using source distributions was disabled. AbiTags { @@ -1138,6 +1143,8 @@ pub(crate) enum PubGrubHint { version: Version, // excluded from `PartialEq` and `Hash` tags: BTreeSet, + // excluded from `PartialEq` and `Hash` + closest: Option<(LanguageTag, AbiTag, String)>, }, /// No wheels are available for a package, and using source distributions was disabled. PlatformTags { @@ -1146,6 +1153,8 @@ pub(crate) enum PubGrubHint { version: Version, // excluded from `PartialEq` and `Hash` tags: Vec, + // excluded from `PartialEq` and `Hash` + closest: Option<(LanguageTag, AbiTag, String)>, }, } @@ -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(", "), + ) + } } } } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 1bb1c58b8487..6bb6fdf9ca06 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -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(()) diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index f1f13ce8245e..4d84017e1995 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -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( @@ -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(