Add error trace to invalid package format (#15626)

In https://github.com/astral-sh/uv/issues/11636, we're getting reports
for installation flakes that report an invalid package format for what
appears to be a network problem. Since we're cutting the error reporting
to the first error message in the chain, we're not reporting the actual
network error underneath it.

This PR displays the whole error chain for invalid package format
errors, so we can debug and eventually catch-and-retry
https://github.com/astral-sh/uv/issues/11636.
This commit is contained in:
konsti 2025-09-02 15:22:42 +02:00 committed by GitHub
parent d70ea34d45
commit 19e19d5795
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 56 additions and 23 deletions

View file

@ -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 {

View file

@ -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<dyn std::error::Error + Send + Sync + 'static>);
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")
}

View file

@ -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;