mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-30 22:11:12 +00:00
Surface invalid metadata as hints in error reports (#2850)
## Summary Closes #2847.
This commit is contained in:
parent
ee9059978a
commit
7ae06b3b46
7 changed files with 229 additions and 27 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4786,6 +4786,7 @@ dependencies = [
|
|||
"requirements-txt",
|
||||
"rkyv",
|
||||
"rustc-hash",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
|
|
@ -48,6 +48,7 @@ petgraph = { workspace = true }
|
|||
pubgrub = { workspace = true }
|
||||
rkyv = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
textwrap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
tokio-stream = { workspace = true }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
|
||||
|
@ -20,7 +20,7 @@ use crate::candidate_selector::CandidateSelector;
|
|||
use crate::dependency_provider::UvDependencyProvider;
|
||||
use crate::pubgrub::{PubGrubPackage, PubGrubPython, PubGrubReportFormatter};
|
||||
use crate::python_requirement::PythonRequirement;
|
||||
use crate::resolver::{UnavailablePackage, VersionsResponse};
|
||||
use crate::resolver::{IncompletePackage, UnavailablePackage, VersionsResponse};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResolveError {
|
||||
|
@ -125,6 +125,7 @@ impl From<pubgrub::error::PubGrubError<UvDependencyProvider>> for ResolveError {
|
|||
python_requirement: None,
|
||||
index_locations: None,
|
||||
unavailable_packages: FxHashMap::default(),
|
||||
incomplete_packages: FxHashMap::default(),
|
||||
})
|
||||
}
|
||||
pubgrub::error::PubGrubError::SelfDependency { package, version } => {
|
||||
|
@ -146,6 +147,7 @@ pub struct NoSolutionError {
|
|||
python_requirement: Option<PythonRequirement>,
|
||||
index_locations: Option<IndexLocations>,
|
||||
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
|
||||
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
|
||||
}
|
||||
|
||||
impl std::error::Error for NoSolutionError {}
|
||||
|
@ -167,6 +169,7 @@ impl std::fmt::Display for NoSolutionError {
|
|||
&self.selector,
|
||||
&self.index_locations,
|
||||
&self.unavailable_packages,
|
||||
&self.incomplete_packages,
|
||||
) {
|
||||
write!(f, "\n\n{hint}")?;
|
||||
}
|
||||
|
@ -261,6 +264,30 @@ impl NoSolutionError {
|
|||
self
|
||||
}
|
||||
|
||||
/// Update the incomplete packages attached to the error.
|
||||
#[must_use]
|
||||
pub(crate) fn with_incomplete_packages(
|
||||
mut self,
|
||||
incomplete_packages: &DashMap<PackageName, DashMap<Version, IncompletePackage>>,
|
||||
) -> Self {
|
||||
let mut new = FxHashMap::default();
|
||||
for package in self.derivation_tree.packages() {
|
||||
if let PubGrubPackage::Package(name, ..) = package {
|
||||
if let Some(entry) = incomplete_packages.get(name) {
|
||||
let versions = entry.value();
|
||||
for entry in versions {
|
||||
let (version, reason) = entry.pair();
|
||||
new.entry(name.clone())
|
||||
.or_insert_with(BTreeMap::default)
|
||||
.insert(version.clone(), reason.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.incomplete_packages = new;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update the Python requirements attached to the error.
|
||||
#[must_use]
|
||||
pub(crate) fn with_python_requirement(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::borrow::Cow;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ops::Bound;
|
||||
|
||||
use derivative::Derivative;
|
||||
|
@ -17,7 +17,7 @@ use uv_normalize::PackageName;
|
|||
|
||||
use crate::candidate_selector::CandidateSelector;
|
||||
use crate::python_requirement::PythonRequirement;
|
||||
use crate::resolver::UnavailablePackage;
|
||||
use crate::resolver::{IncompletePackage, UnavailablePackage};
|
||||
|
||||
use super::PubGrubPackage;
|
||||
|
||||
|
@ -342,6 +342,7 @@ impl PubGrubReportFormatter<'_> {
|
|||
selector: &Option<CandidateSelector>,
|
||||
index_locations: &Option<IndexLocations>,
|
||||
unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
|
||||
incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, IncompletePackage>>,
|
||||
) -> IndexSet<PubGrubHint> {
|
||||
/// Returns `true` if pre-releases were allowed for a package.
|
||||
fn allowed_prerelease(package: &PubGrubPackage, selector: &CandidateSelector) -> bool {
|
||||
|
@ -354,7 +355,7 @@ impl PubGrubReportFormatter<'_> {
|
|||
let mut hints = IndexSet::default();
|
||||
match derivation_tree {
|
||||
DerivationTree::External(external) => match external {
|
||||
External::NoVersions(package, set, _) => {
|
||||
External::Unavailable(package, set, _) | External::NoVersions(package, set, _) => {
|
||||
// Check for no versions due to pre-release options
|
||||
if let Some(selector) = selector {
|
||||
let any_prerelease = set.iter().any(|(start, end)| {
|
||||
|
@ -404,6 +405,7 @@ impl PubGrubReportFormatter<'_> {
|
|||
index_locations.flat_index().peekable().peek().is_none();
|
||||
|
||||
if let PubGrubPackage::Package(name, ..) = package {
|
||||
// Add hints due to the package being entirely unavailable.
|
||||
match unavailable_packages.get(name) {
|
||||
Some(UnavailablePackage::NoIndex) => {
|
||||
if no_find_links {
|
||||
|
@ -413,13 +415,55 @@ impl PubGrubReportFormatter<'_> {
|
|||
Some(UnavailablePackage::Offline) => {
|
||||
hints.insert(PubGrubHint::Offline);
|
||||
}
|
||||
_ => {}
|
||||
Some(UnavailablePackage::InvalidMetadata(reason)) => {
|
||||
hints.insert(PubGrubHint::InvalidPackageMetadata {
|
||||
package: package.clone(),
|
||||
reason: reason.clone(),
|
||||
});
|
||||
}
|
||||
Some(UnavailablePackage::InvalidStructure(reason)) => {
|
||||
hints.insert(PubGrubHint::InvalidPackageStructure {
|
||||
package: package.clone(),
|
||||
reason: reason.clone(),
|
||||
});
|
||||
}
|
||||
Some(UnavailablePackage::NotFound) => {}
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Add hints due to the package being unavailable at specific versions.
|
||||
if let Some(versions) = incomplete_packages.get(name) {
|
||||
for (version, incomplete) in versions.iter().rev() {
|
||||
if set.contains(version) {
|
||||
match incomplete {
|
||||
IncompletePackage::Offline => {
|
||||
hints.insert(PubGrubHint::Offline);
|
||||
}
|
||||
IncompletePackage::InvalidMetadata(reason) => {
|
||||
hints.insert(PubGrubHint::InvalidVersionMetadata {
|
||||
package: package.clone(),
|
||||
version: version.clone(),
|
||||
reason: reason.clone(),
|
||||
});
|
||||
}
|
||||
IncompletePackage::InvalidStructure(reason) => {
|
||||
hints.insert(
|
||||
PubGrubHint::InvalidVersionStructure {
|
||||
package: package.clone(),
|
||||
version: version.clone(),
|
||||
reason: reason.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
External::NotRoot(..) => {}
|
||||
External::Unavailable(..) => {}
|
||||
External::FromDependencyOf(..) => {}
|
||||
},
|
||||
DerivationTree::Derived(derived) => {
|
||||
|
@ -428,12 +472,14 @@ impl PubGrubReportFormatter<'_> {
|
|||
selector,
|
||||
index_locations,
|
||||
unavailable_packages,
|
||||
incomplete_packages,
|
||||
));
|
||||
hints.extend(self.hints(
|
||||
&derived.cause2,
|
||||
selector,
|
||||
index_locations,
|
||||
unavailable_packages,
|
||||
incomplete_packages,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -462,8 +508,36 @@ pub(crate) enum PubGrubHint {
|
|||
/// Requirements were unavailable due to lookups in the index being disabled and no extra
|
||||
/// index was provided via `--find-links`
|
||||
NoIndex,
|
||||
/// A package was not found in the registry, but
|
||||
/// A package was not found in the registry, but network access was disabled.
|
||||
Offline,
|
||||
/// Metadata for a package could not be parsed.
|
||||
InvalidPackageMetadata {
|
||||
package: PubGrubPackage,
|
||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||
reason: String,
|
||||
},
|
||||
/// The structure of a package was invalid (e.g., multiple `.dist-info` directories).
|
||||
InvalidPackageStructure {
|
||||
package: PubGrubPackage,
|
||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||
reason: String,
|
||||
},
|
||||
/// Metadata for a package version could not be parsed.
|
||||
InvalidVersionMetadata {
|
||||
package: PubGrubPackage,
|
||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||
version: Version,
|
||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||
reason: String,
|
||||
},
|
||||
/// The structure of a package version was invalid (e.g., multiple `.dist-info` directories).
|
||||
InvalidVersionStructure {
|
||||
package: PubGrubPackage,
|
||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||
version: Version,
|
||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PubGrubHint {
|
||||
|
@ -505,6 +579,56 @@ impl std::fmt::Display for PubGrubHint {
|
|||
":".bold(),
|
||||
)
|
||||
}
|
||||
Self::InvalidPackageMetadata { package, reason } => {
|
||||
write!(
|
||||
f,
|
||||
"{}{} Metadata for {} could not be parsed:\n{}",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.bold(),
|
||||
textwrap::indent(reason, " ")
|
||||
)
|
||||
}
|
||||
Self::InvalidPackageStructure { package, reason } => {
|
||||
write!(
|
||||
f,
|
||||
"{}{} The structure of {} was invalid:\n{}",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.bold(),
|
||||
textwrap::indent(reason, " ")
|
||||
)
|
||||
}
|
||||
Self::InvalidVersionMetadata {
|
||||
package,
|
||||
version,
|
||||
reason,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"{}{} Metadata for {}=={} could not be parsed:\n{}",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.bold(),
|
||||
version.bold(),
|
||||
textwrap::indent(reason, " ")
|
||||
)
|
||||
}
|
||||
Self::InvalidVersionStructure {
|
||||
package,
|
||||
version,
|
||||
reason,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"{}{} The structure of {}=={} was invalid:\n{}",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
package.bold(),
|
||||
version.bold(),
|
||||
textwrap::indent(reason, " ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,19 +71,30 @@ pub(crate) enum UnavailableVersion {
|
|||
IncompatibleDist(IncompatibleDist),
|
||||
}
|
||||
|
||||
/// The package is unavailable and cannot be used
|
||||
/// The package is unavailable and cannot be used.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum UnavailablePackage {
|
||||
/// Index lookups were disabled (i.e., `--no-index`) and the package was not found in a flat index (i.e. from `--find-links`)
|
||||
/// Index lookups were disabled (i.e., `--no-index`) and the package was not found in a flat index (i.e. from `--find-links`).
|
||||
NoIndex,
|
||||
/// Network requests were disabled (i.e., `--offline`), and the package was not found in the cache.
|
||||
Offline,
|
||||
/// The package was not found in the registry
|
||||
/// The package was not found in the registry.
|
||||
NotFound,
|
||||
/// The package metadata was found, but could not be parsed.
|
||||
InvalidMetadata(String),
|
||||
/// The package has an invalid structure.
|
||||
InvalidStructure(String),
|
||||
}
|
||||
|
||||
/// The package is unavailable at specific versions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum IncompletePackage {
|
||||
/// Network requests were disabled (i.e., `--offline`), and the wheel metadata was not found in the cache.
|
||||
Offline,
|
||||
/// The wheel metadata was found, but could not be parsed.
|
||||
InvalidMetadata,
|
||||
InvalidMetadata(String),
|
||||
/// The wheel has an invalid structure.
|
||||
InvalidStructure,
|
||||
InvalidStructure(String),
|
||||
}
|
||||
|
||||
enum ResolverVersion {
|
||||
|
@ -113,8 +124,10 @@ pub struct Resolver<
|
|||
selector: CandidateSelector,
|
||||
index: &'a InMemoryIndex,
|
||||
installed_packages: &'a InstalledPackages,
|
||||
/// Incompatibilities for packages that are entirely unavailable
|
||||
/// Incompatibilities for packages that are entirely unavailable.
|
||||
unavailable_packages: DashMap<PackageName, UnavailablePackage>,
|
||||
/// Incompatibilities for packages that are unavailable at specific versions.
|
||||
incomplete_packages: DashMap<PackageName, DashMap<Version, IncompletePackage>>,
|
||||
/// The set of all registry-based packages visited during resolution.
|
||||
visited: DashSet<PackageName>,
|
||||
reporter: Option<Arc<dyn Reporter>>,
|
||||
|
@ -185,6 +198,7 @@ impl<
|
|||
Ok(Self {
|
||||
index,
|
||||
unavailable_packages: DashMap::default(),
|
||||
incomplete_packages: DashMap::default(),
|
||||
visited: DashSet::default(),
|
||||
selector: CandidateSelector::for_resolution(options, &manifest, markers),
|
||||
dependency_mode: options.dependency_mode,
|
||||
|
@ -247,7 +261,8 @@ impl<
|
|||
.with_selector(self.selector.clone())
|
||||
.with_python_requirement(&self.python_requirement)
|
||||
.with_index_locations(self.provider.index_locations())
|
||||
.with_unavailable_packages(&self.unavailable_packages),
|
||||
.with_unavailable_packages(&self.unavailable_packages)
|
||||
.with_incomplete_packages(&self.incomplete_packages),
|
||||
)
|
||||
} else {
|
||||
err
|
||||
|
@ -352,10 +367,10 @@ impl<
|
|||
UnavailablePackage::NotFound => {
|
||||
"was not found in the package registry"
|
||||
}
|
||||
UnavailablePackage::InvalidMetadata => {
|
||||
UnavailablePackage::InvalidMetadata(_) => {
|
||||
"was found, but the metadata could not be parsed"
|
||||
}
|
||||
UnavailablePackage::InvalidStructure => {
|
||||
UnavailablePackage::InvalidStructure(_) => {
|
||||
"was found, but has an invalid format"
|
||||
}
|
||||
})
|
||||
|
@ -624,14 +639,18 @@ impl<
|
|||
.insert(package_name.clone(), UnavailablePackage::Offline);
|
||||
return Ok(None);
|
||||
}
|
||||
MetadataResponse::InvalidMetadata(_) => {
|
||||
self.unavailable_packages
|
||||
.insert(package_name.clone(), UnavailablePackage::InvalidMetadata);
|
||||
MetadataResponse::InvalidMetadata(err) => {
|
||||
self.unavailable_packages.insert(
|
||||
package_name.clone(),
|
||||
UnavailablePackage::InvalidMetadata(err.to_string()),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
MetadataResponse::InvalidStructure(_) => {
|
||||
self.unavailable_packages
|
||||
.insert(package_name.clone(), UnavailablePackage::InvalidStructure);
|
||||
MetadataResponse::InvalidStructure(err) => {
|
||||
self.unavailable_packages.insert(
|
||||
package_name.clone(),
|
||||
UnavailablePackage::InvalidStructure(err.to_string()),
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
@ -913,20 +932,38 @@ impl<
|
|||
.await
|
||||
.ok_or(ResolveError::Unregistered)?;
|
||||
|
||||
let metadata = match *response {
|
||||
MetadataResponse::Found(ref metadata) => metadata,
|
||||
let metadata = match &*response {
|
||||
MetadataResponse::Found(metadata) => metadata,
|
||||
MetadataResponse::Offline => {
|
||||
self.incomplete_packages
|
||||
.entry(package_name.clone())
|
||||
.or_default()
|
||||
.insert(version.clone(), IncompletePackage::Offline);
|
||||
return Ok(Dependencies::Unavailable(
|
||||
"network connectivity is disabled, but the metadata wasn't found in the cache"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
MetadataResponse::InvalidMetadata(_) => {
|
||||
MetadataResponse::InvalidMetadata(err) => {
|
||||
self.incomplete_packages
|
||||
.entry(package_name.clone())
|
||||
.or_default()
|
||||
.insert(
|
||||
version.clone(),
|
||||
IncompletePackage::InvalidMetadata(err.to_string()),
|
||||
);
|
||||
return Ok(Dependencies::Unavailable(
|
||||
"the package metadata could not be parsed".to_string(),
|
||||
));
|
||||
}
|
||||
MetadataResponse::InvalidStructure(_) => {
|
||||
MetadataResponse::InvalidStructure(err) => {
|
||||
self.incomplete_packages
|
||||
.entry(package_name.clone())
|
||||
.or_default()
|
||||
.insert(
|
||||
version.clone(),
|
||||
IncompletePackage::InvalidStructure(err.to_string()),
|
||||
);
|
||||
return Ok(Dependencies::Unavailable(
|
||||
"the package has an invalid format".to_string(),
|
||||
));
|
||||
|
|
|
@ -4555,6 +4555,12 @@ fn invalid_metadata_requires_python() -> Result<()> {
|
|||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because validation==2.0.0 is unusable because the package metadata could not be parsed and you require validation==2.0.0, we can conclude that the requirements are unsatisfiable.
|
||||
|
||||
hint: Metadata for validation==2.0.0 could not be parsed:
|
||||
Failed to parse version: Unexpected end of version specifier, expected operator:
|
||||
12
|
||||
^^
|
||||
|
||||
"###
|
||||
);
|
||||
|
||||
|
@ -4581,6 +4587,9 @@ fn invalid_metadata_multiple_dist_info() -> Result<()> {
|
|||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because validation==3.0.0 is unusable because the package has an invalid format and you require validation==3.0.0, we can conclude that the requirements are unsatisfiable.
|
||||
|
||||
hint: The structure of validation==3.0.0 was invalid:
|
||||
Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
@ -1106,6 +1106,9 @@ fn mismatched_name() -> Result<()> {
|
|||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because foo was found, but has an invalid format and you require foo, we can conclude that the 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
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue