Skip to content

Commit

Permalink
Show expected and available ABI tags in resolver errors (#10527)
Browse files Browse the repository at this point in the history
## Summary

The idea here is to show both (1) an example of a compatible tag and (2)
the tags that were available, whenever we fail to resolve due to an
abscence of matching wheels.

Closes #2777.
  • Loading branch information
charliermarsh authored Jan 14, 2025
1 parent e0e8ba5 commit 2ffa319
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 65 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/uv-distribution-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ bitflags = { workspace = true }
fs-err = { workspace = true }
itertools = { workspace = true }
jiff = { workspace = true }
owo-colors = { workspace = true }
petgraph = { workspace = true }
rkyv = { workspace = true }
rustc-hash = { workspace = true }
Expand Down
74 changes: 73 additions & 1 deletion crates/uv-distribution-types/src/prioritized_distribution.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::collections::BTreeSet;
use std::fmt::{Display, Formatter};

use arcstr::ArcStr;
use owo_colors::OwoColorize;
use tracing::debug;

use uv_distribution_filename::{BuildTag, WheelFilename};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{IncompatibleTag, TagPriority};
use uv_platform_tags::{AbiTag, IncompatibleTag, TagPriority, Tags};
use uv_pypi_types::{HashDigest, Yanked};

use crate::{
Expand Down Expand Up @@ -164,6 +166,40 @@ impl IncompatibleDist {
Self::Unavailable => format!("have {self}"),
}
}

pub fn context_message(
&self,
tags: Option<&Tags>,
requires_python: Option<AbiTag>,
) -> Option<String> {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::Tag(IncompatibleTag::Python) => {
let tag = tags?.python_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Abi) => {
let tag = tags?.abi_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => {
let tag = requires_python?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Platform) => {
let tag = tags?.platform_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Invalid) => None,
IncompatibleWheel::NoBinary => None,
IncompatibleWheel::Yanked(..) => None,
IncompatibleWheel::ExcludeNewer(..) => None,
IncompatibleWheel::RequiresPython(..) => None,
},
Self::Source(..) => None,
Self::Unavailable => None,
}
}
}

impl Display for IncompatibleDist {
Expand Down Expand Up @@ -246,6 +282,8 @@ pub enum IncompatibleWheel {
/// The wheel tags do not match those of the target Python platform.
Tag(IncompatibleTag),
/// The required Python version is not a superset of the target Python version range.
///
/// TODO(charlie): Consider making this two variants to reduce enum size.
RequiresPython(VersionSpecifiers, PythonRequirementKind),
/// The wheel was yanked.
Yanked(Yanked),
Expand Down Expand Up @@ -483,6 +521,40 @@ impl PrioritizedDist {
pub fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
self.0.best_wheel_index.map(|i| &self.0.wheels[i])
}

/// Returns the set of all Python tags for the distribution.
pub fn python_tags(&self) -> BTreeSet<&str> {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.python_tag.iter().map(String::as_str))
.collect()
}

/// Returns the set of all ABI tags for the distribution.
pub fn abi_tags(&self) -> BTreeSet<&str> {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.abi_tag.iter().map(String::as_str))
.collect()
}

/// Returns the set of platform tags for the distribution that are ABI-compatible with the given
/// tags.
pub fn platform_tags<'a>(&'a self, tags: &'a Tags) -> BTreeSet<&'a str> {
let mut candidates = BTreeSet::new();
for (wheel, _) in &self.0.wheels {
for wheel_py in &wheel.filename.python_tag {
for wheel_abi in &wheel.filename.abi_tag {
if tags.is_compatible_abi(wheel_py.as_str(), wheel_abi.as_str()) {
candidates.extend(wheel.filename.platform_tag.iter().map(String::as_str));
}
}
}
}
candidates
}
}

impl<'a> CompatibleDist<'a> {
Expand Down
38 changes: 35 additions & 3 deletions crates/uv-platform-tags/src/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ use std::{cmp, num::NonZeroU32};

use rustc_hash::FxHashMap;

use crate::abi_tag::AbiTag;
use crate::{Arch, LanguageTag, Os, Platform, PlatformError};
use crate::{AbiTag, Arch, LanguageTag, Os, Platform, PlatformError};

#[derive(Debug, thiserror::Error)]
pub enum TagsError {
Expand Down Expand Up @@ -75,6 +74,8 @@ pub struct Tags {
/// `python_tag` |--> `abi_tag` |--> `platform_tag` |--> priority
#[allow(clippy::type_complexity)]
map: Arc<FxHashMap<String, FxHashMap<String, FxHashMap<String, TagPriority>>>>,
/// The highest-priority tag for the Python version and platform.
best: Option<(String, String, String)>,
}

impl Tags {
Expand All @@ -83,6 +84,9 @@ impl Tags {
/// Tags are prioritized based on their position in the given vector. Specifically, tags that
/// appear earlier in the vector are given higher priority than tags that appear later.
pub fn new(tags: Vec<(String, String, String)>) -> Self {
// Store the highest-priority tag for each component.
let best = tags.first().cloned();

// Index the tags by Python version, ABI, and platform.
let mut map = FxHashMap::default();
for (index, (py, abi, platform)) in tags.into_iter().rev().enumerate() {
Expand All @@ -93,7 +97,11 @@ impl Tags {
.entry(platform)
.or_insert(TagPriority::try_from(index).expect("valid tag priority"));
}
Self { map: Arc::new(map) }

Self {
map: Arc::new(map),
best,
}
}

/// Returns the compatible tags for the given Python implementation (e.g., `cpython`), version,
Expand Down Expand Up @@ -291,6 +299,30 @@ impl Tags {
}
max_compatibility
}

/// Return the highest-priority Python tag for the [`Tags`].
pub fn python_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(py, _, _)| py.as_str())
}

/// Return the highest-priority ABI tag for the [`Tags`].
pub fn abi_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(_, abi, _)| abi.as_str())
}

/// Return the highest-priority platform tag for the [`Tags`].
pub fn platform_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(_, _, platform)| platform.as_str())
}

/// Returns `true` if the given language and ABI tags are compatible with the current
/// environment.
pub fn is_compatible_abi<'a>(&'a self, python_tag: &'a str, abi_tag: &'a str) -> bool {
self.map
.get(python_tag)
.map(|abis| abis.contains_key(abi_tag))
.unwrap_or(false)
}
}

/// The priority of a platform tag.
Expand Down
10 changes: 10 additions & 0 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl CandidateSelector {
return Some(Candidate {
name: package_name,
version,
prioritized: None,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(
dist,
)),
Expand Down Expand Up @@ -302,6 +303,7 @@ impl CandidateSelector {
return Some(Candidate {
name: package_name,
version,
prioritized: None,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(dist)),
choice_kind: VersionChoiceKind::Installed,
});
Expand Down Expand Up @@ -583,6 +585,8 @@ pub(crate) struct Candidate<'a> {
name: &'a PackageName,
/// The version of the package.
version: &'a Version,
/// The prioritized distribution for the package.
prioritized: Option<&'a PrioritizedDist>,
/// The distributions to use for resolving and installing the package.
dist: CandidateDist<'a>,
/// Whether this candidate was selected from a preference.
Expand All @@ -599,6 +603,7 @@ impl<'a> Candidate<'a> {
Self {
name,
version,
prioritized: Some(dist),
dist: CandidateDist::from(dist),
choice_kind,
}
Expand Down Expand Up @@ -632,6 +637,11 @@ impl<'a> Candidate<'a> {
pub(crate) fn dist(&self) -> &CandidateDist<'a> {
&self.dist
}

/// Return the prioritized distribution for the candidate.
pub(crate) fn prioritized(&self) -> Option<&PrioritizedDist> {
self.prioritized
}
}

impl Name for Candidate<'_> {
Expand Down
58 changes: 56 additions & 2 deletions crates/uv-resolver/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ use uv_distribution_types::{
};
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{LocalVersionSlice, Version};
use uv_platform_tags::Tags;
use uv_static::EnvVars;

use crate::candidate_selector::CandidateSelector;
use crate::dependency_provider::UvDependencyProvider;
use crate::fork_indexes::ForkIndexes;
use crate::fork_urls::ForkUrls;
use crate::prerelease::AllowPrerelease;
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter};
Expand All @@ -27,7 +29,7 @@ use crate::resolution::ConflictingDistributionError;
use crate::resolver::{
MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
};
use crate::Options;
use crate::{InMemoryIndex, Options};

#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
Expand Down Expand Up @@ -130,9 +132,9 @@ impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
pub(crate) type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>;

/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
#[derive(Debug)]
pub struct NoSolutionError {
error: pubgrub::NoSolutionError<UvDependencyProvider>,
index: InMemoryIndex,
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
selector: CandidateSelector,
Expand All @@ -142,7 +144,9 @@ pub struct NoSolutionError {
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
fork_urls: ForkUrls,
fork_indexes: ForkIndexes,
env: ResolverEnvironment,
tags: Option<Tags>,
workspace_members: BTreeSet<PackageName>,
options: Options,
}
Expand All @@ -151,6 +155,7 @@ impl NoSolutionError {
/// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
pub(crate) fn new(
error: pubgrub::NoSolutionError<UvDependencyProvider>,
index: InMemoryIndex,
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
selector: CandidateSelector,
Expand All @@ -160,12 +165,15 @@ impl NoSolutionError {
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
fork_urls: ForkUrls,
fork_indexes: ForkIndexes,
env: ResolverEnvironment,
tags: Option<Tags>,
workspace_members: BTreeSet<PackageName>,
options: Options,
) -> Self {
Self {
error,
index,
available_versions,
available_indexes,
selector,
Expand All @@ -175,7 +183,9 @@ impl NoSolutionError {
unavailable_packages,
incomplete_packages,
fork_urls,
fork_indexes,
env,
tags,
workspace_members,
options,
}
Expand Down Expand Up @@ -328,6 +338,47 @@ impl NoSolutionError {
}
}

impl std::fmt::Debug for NoSolutionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// Include every field except `index`, which doesn't implement `Debug`.
let Self {
error,
index: _,
available_versions,
available_indexes,
selector,
python_requirement,
index_locations,
index_capabilities,
unavailable_packages,
incomplete_packages,
fork_urls,
fork_indexes,
env,
tags,
workspace_members,
options,
} = self;
f.debug_struct("NoSolutionError")
.field("error", error)
.field("available_versions", available_versions)
.field("available_indexes", available_indexes)
.field("selector", selector)
.field("python_requirement", python_requirement)
.field("index_locations", index_locations)
.field("index_capabilities", index_capabilities)
.field("unavailable_packages", unavailable_packages)
.field("incomplete_packages", incomplete_packages)
.field("fork_urls", fork_urls)
.field("fork_indexes", fork_indexes)
.field("env", env)
.field("tags", tags)
.field("workspace_members", workspace_members)
.field("options", options)
.finish()
}
}

impl std::error::Error for NoSolutionError {}

impl std::fmt::Display for NoSolutionError {
Expand All @@ -337,6 +388,7 @@ impl std::fmt::Display for NoSolutionError {
available_versions: &self.available_versions,
python_requirement: &self.python_requirement,
workspace_members: &self.workspace_members,
tags: self.tags.as_ref(),
};

// Transform the error tree for reporting
Expand Down Expand Up @@ -385,13 +437,15 @@ impl std::fmt::Display for NoSolutionError {
let mut additional_hints = IndexSet::default();
formatter.generate_hints(
&tree,
&self.index,
&self.selector,
&self.index_locations,
&self.index_capabilities,
&self.available_indexes,
&self.unavailable_packages,
&self.incomplete_packages,
&self.fork_urls,
&self.fork_indexes,
&self.env,
&self.workspace_members,
&self.options,
Expand Down
Loading

0 comments on commit 2ffa319

Please sign in to comment.