Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions crates/uv-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 36 additions & 6 deletions crates/uv-resolver/src/resolver/availability.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -119,6 +122,29 @@ impl From<&MetadataUnavailable> for UnavailableVersion {
}
}

/// Display the error chain for unavailable packages.
#[derive(Debug, Clone)]
pub struct UnavailableErrorChain(Arc<dyn std::error::Error + Send + Sync + 'static>);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name and location of the type is up for bikeshedding. Maybe we want a generic version in uv-warnings?


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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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::InvalidMetadata(err) => {
Self::InvalidMetadata(UnavailableErrorChain(err.clone()))
}
MetadataUnavailable::InconsistentMetadata(err) => {
Self::InvalidMetadata(err.to_string())
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")
}
Expand Down
3 changes: 2 additions & 1 deletion crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 9 additions & 8 deletions crates/uv/tests/it/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand All @@ -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(())
Expand Down Expand Up @@ -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 -----
Expand All @@ -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
Comment on lines +2625 to +2627
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a chance here that user's mistake this hint for an error message (since we're using the familiar error format), not sure if we can make it clearer that the resolution error trace above is the actual error.

"
);

Ok(())
Expand Down