diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 1c7bfceb2..48b7b60e7 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -25,7 +25,8 @@ use crate::prerelease::AllowPrerelease; use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; use crate::python_requirement::{PythonRequirement, PythonRequirementSource}; use crate::resolver::{ - MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion, + MetadataUnavailable, UnavailableErrorChain, UnavailablePackage, UnavailableReason, + UnavailableVersion, }; use crate::{Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse}; @@ -1022,13 +1023,13 @@ pub(crate) enum PubGrubHint { InvalidPackageMetadata { package: PackageName, // excluded from `PartialEq` and `Hash` - reason: String, + reason: UnavailableErrorChain, }, /// The structure of a package was invalid (e.g., multiple `.dist-info` directories). InvalidPackageStructure { package: PackageName, // excluded from `PartialEq` and `Hash` - reason: String, + reason: UnavailableErrorChain, }, /// Metadata for a package version could not be parsed. InvalidVersionMetadata { @@ -1344,21 +1345,21 @@ impl std::fmt::Display for PubGrubHint { Self::InvalidPackageMetadata { package, reason } => { write!( f, - "{}{} Metadata for `{}` could not be parsed:\n{}", + "{}{} Metadata for `{}` could not be parsed.\n{}", "hint".bold().cyan(), ":".bold(), package.cyan(), - textwrap::indent(reason, " ") + textwrap::indent(reason.to_string().as_str(), " ") ) } Self::InvalidPackageStructure { package, reason } => { write!( f, - "{}{} The structure of `{}` was invalid:\n{}", + "{}{} The structure of `{}` was invalid\n{}", "hint".bold().cyan(), ":".bold(), package.cyan(), - textwrap::indent(reason, " ") + textwrap::indent(reason.to_string().as_str(), " ") ) } Self::InvalidVersionMetadata { diff --git a/crates/uv-resolver/src/resolver/availability.rs b/crates/uv-resolver/src/resolver/availability.rs index 0015199be..278d31550 100644 --- a/crates/uv-resolver/src/resolver/availability.rs +++ b/crates/uv-resolver/src/resolver/availability.rs @@ -1,10 +1,13 @@ use std::fmt::{Display, Formatter}; +use std::iter; +use std::sync::Arc; -use crate::resolver::{MetadataUnavailable, VersionFork}; use uv_distribution_types::IncompatibleDist; use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::{AbiTag, Tags}; +use crate::resolver::{MetadataUnavailable, VersionFork}; + /// The reason why a package or a version cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] pub enum UnavailableReason { @@ -119,6 +122,29 @@ impl From<&MetadataUnavailable> for UnavailableVersion { } } +/// Display the error chain for unavailable packages. +#[derive(Debug, Clone)] +pub struct UnavailableErrorChain(Arc); + +impl Display for UnavailableErrorChain { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for source in iter::successors(Some(&self.0 as &dyn std::error::Error), |&err| err.source()) + { + writeln!(f, "Caused by: {}", source.to_string().trim())?; + } + Ok(()) + } +} + +impl PartialEq for UnavailableErrorChain { + /// Whether we can collapse two reasons into one because they would be rendered the same. + fn eq(&self, other: &Self) -> bool { + self.to_string() == other.to_string() + } +} + +impl Eq for UnavailableErrorChain {} + /// The package is unavailable and cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] pub enum UnavailablePackage { @@ -129,9 +155,9 @@ pub enum UnavailablePackage { /// The package was not found in the registry. NotFound, /// The package metadata was found, but could not be parsed. - InvalidMetadata(String), + InvalidMetadata(UnavailableErrorChain), /// The package has an invalid structure. - InvalidStructure(String), + InvalidStructure(UnavailableErrorChain), } impl UnavailablePackage { @@ -166,11 +192,15 @@ impl From<&MetadataUnavailable> for UnavailablePackage { fn from(reason: &MetadataUnavailable) -> Self { match reason { MetadataUnavailable::Offline => Self::Offline, - MetadataUnavailable::InvalidMetadata(err) => Self::InvalidMetadata(err.to_string()), - MetadataUnavailable::InconsistentMetadata(err) => { - Self::InvalidMetadata(err.to_string()) + MetadataUnavailable::InvalidMetadata(err) => { + Self::InvalidMetadata(UnavailableErrorChain(err.clone())) + } + MetadataUnavailable::InconsistentMetadata(err) => { + Self::InvalidMetadata(UnavailableErrorChain(err.clone())) + } + MetadataUnavailable::InvalidStructure(err) => { + Self::InvalidStructure(UnavailableErrorChain(err.clone())) } - MetadataUnavailable::InvalidStructure(err) => Self::InvalidStructure(err.to_string()), MetadataUnavailable::RequiresPython(..) => { unreachable!("`requires-python` is only known upfront for registry distributions") } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 0ec707a40..c0b558dc8 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -56,7 +56,8 @@ use crate::python_requirement::PythonRequirement; use crate::resolution::ResolverOutput; use crate::resolution_mode::ResolutionStrategy; pub(crate) use crate::resolver::availability::{ - ResolverVersion, UnavailablePackage, UnavailableReason, UnavailableVersion, + ResolverVersion, UnavailableErrorChain, UnavailablePackage, UnavailableReason, + UnavailableVersion, }; use crate::resolver::batch_prefetch::BatchPrefetcher; pub use crate::resolver::derivation::DerivationChainBuilder; diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 30cc315c1..663da1a11 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -1269,7 +1269,7 @@ fn mismatched_name() -> Result<()> { uv_snapshot!(context.filters(), context.pip_sync() .arg("requirements.txt") - .arg("--strict"), @r###" + .arg("--strict"), @r" success: false exit_code: 1 ----- stdout ----- @@ -1278,9 +1278,9 @@ fn mismatched_name() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because foo has an invalid package format and you require foo, we can conclude that your requirements are unsatisfiable. - hint: The structure of `foo` was invalid: - The .dist-info directory tomli-2.0.1 does not start with the normalized package name: foo - "### + hint: The structure of `foo` was invalid + Caused by: The .dist-info directory tomli-2.0.1 does not start with the normalized package name: foo + " ); Ok(()) @@ -2613,7 +2613,7 @@ fn incompatible_wheel() -> Result<()> { uv_snapshot!(context.filters(), context.pip_sync() .arg("requirements.txt") - .arg("--strict"), @r###" + .arg("--strict"), @r" success: false exit_code: 1 ----- stdout ----- @@ -2622,9 +2622,10 @@ fn incompatible_wheel() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because foo has an invalid package format and you require foo, we can conclude that your requirements are unsatisfiable. - hint: The structure of `foo` was invalid: - Failed to read from zip file - "### + hint: The structure of `foo` was invalid + Caused by: Failed to read from zip file + Caused by: unable to locate the end of central directory record + " ); Ok(())