Use generic pubgrub incompatibility reason (#3335)

Pubgrub got a new feature where all unavailability is a custom, instead
of the reasonless `UnavailableDependencies` and our custom `String` type
previously (https://github.com/pubgrub-rs/pubgrub/pull/208). This PR
introduces a `UnavailableReason` that tracks either an entire version
being unusable, or a specific version. The error messages now also track
this difference properly.

The pubgrub commit is our main rebased onto the merged
https://github.com/pubgrub-rs/pubgrub/pull/208, i'll push
`konsti/main-rebase-generic-reason` to `main` after checking for rebase
problems.
This commit is contained in:
konsti 2024-05-08 10:40:15 +02:00 committed by GitHub
parent bd7860de17
commit 1ad6aa8a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 215 additions and 165 deletions

View file

@ -6,6 +6,7 @@ use pubgrub::solver::{Dependencies, DependencyProvider};
use pep440_rs::Version;
use crate::pubgrub::{PubGrubPackage, PubGrubPriority};
use crate::resolver::UnavailableReason;
/// We don't use a dependency provider, we interact with state directly, but we still need this one
/// for type
@ -15,6 +16,8 @@ impl DependencyProvider for UvDependencyProvider {
type P = PubGrubPackage;
type V = Version;
type VS = Range<Version>;
type M = UnavailableReason;
fn prioritize(&self, _package: &Self::P, _range: &Self::VS) -> Self::Priority {
unimplemented!()
}
@ -34,7 +37,7 @@ impl DependencyProvider for UvDependencyProvider {
&self,
_package: &Self::P,
_version: &Self::V,
) -> Result<Dependencies<Vec<(Self::P, Self::VS)>>, Self::Err> {
) -> Result<Dependencies<Vec<(Self::P, Self::VS)>, Self::M>, Self::Err> {
unimplemented!()
}
}

View file

@ -23,7 +23,8 @@ use crate::dependency_provider::UvDependencyProvider;
use crate::pubgrub::{PubGrubPackage, PubGrubPython, PubGrubReportFormatter};
use crate::python_requirement::PythonRequirement;
use crate::resolver::{
IncompletePackage, SharedMap, SharedSet, UnavailablePackage, VersionsResponse,
IncompletePackage, SharedMap, SharedSet, UnavailablePackage, UnavailableReason,
VersionsResponse,
};
#[derive(Debug, thiserror::Error)]
@ -119,7 +120,9 @@ impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
/// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
/// wrap an [`PubGrubPackage::Extra`] package.
fn collapse_extra_proxies(derivation_tree: &mut DerivationTree<PubGrubPackage, Range<Version>>) {
fn collapse_extra_proxies(
derivation_tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
) {
match derivation_tree {
DerivationTree::External(_) => {}
DerivationTree::Derived(derived) => {
@ -193,7 +196,7 @@ impl From<pubgrub::error::PubGrubError<UvDependencyProvider>> for ResolveError {
/// A wrapper around [`pubgrub::error::PubGrubError::NoSolution`] that displays a resolution failure report.
#[derive(Debug)]
pub struct NoSolutionError {
derivation_tree: DerivationTree<PubGrubPackage, Range<Version>>,
derivation_tree: DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
available_versions: IndexMap<PubGrubPackage, BTreeSet<Version>>,
selector: Option<CandidateSelector>,
python_requirement: Option<PythonRequirement>,

View file

@ -17,7 +17,7 @@ use uv_normalize::PackageName;
use crate::candidate_selector::CandidateSelector;
use crate::python_requirement::PythonRequirement;
use crate::resolver::{IncompletePackage, UnavailablePackage};
use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason};
use super::PubGrubPackage;
@ -30,15 +30,20 @@ pub(crate) struct PubGrubReportFormatter<'a> {
pub(crate) python_requirement: Option<&'a PythonRequirement>,
}
impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<'_> {
impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
for PubGrubReportFormatter<'_>
{
type Output = String;
fn format_external(&self, external: &External<PubGrubPackage, Range<Version>>) -> Self::Output {
fn format_external(
&self,
external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
) -> Self::Output {
match external {
External::NotRoot(package, version) => {
format!("we are solving dependencies of {package} {version}")
}
External::NoVersions(package, set, reason) => {
External::NoVersions(package, set) => {
if matches!(package, PubGrubPackage::Python(_)) {
if let Some(python) = self.python_requirement {
if python.target() == python.installed() {
@ -79,16 +84,6 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
}
let set = self.simplify_set(set, package);
// Check for a reason
if let Some(reason) = reason {
let formatted = if set.as_ref() == &Range::full() {
format!("{package} {reason}")
} else {
format!("{package}{set} {reason}")
};
return formatted;
}
if set.as_ref() == &Range::full() {
format!("there are no versions of {package}")
} else if set.as_singleton().is_some() {
@ -112,17 +107,26 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
}
}
}
External::Unavailable(package, set, reason) => match package {
External::Custom(package, set, reason) => match package {
PubGrubPackage::Root(Some(name)) => {
format!("{name} cannot be used because {reason}")
}
PubGrubPackage::Root(None) => {
format!("your requirements cannot be used because {reason}")
}
_ => format!(
"{}is unusable because {reason}",
Padded::new("", &PackageRange::compatibility(package, set), " ")
),
_ => match reason {
UnavailableReason::Package(reason) => {
// While there may be a term attached, this error applies to the entire
// package, so we show it for the entire package
format!("{}{reason}", Padded::new("", &package, " "))
}
UnavailableReason::Version(reason) => {
format!(
"{}{reason}",
Padded::new("", &PackageRange::compatibility(package, set), " ")
)
}
},
},
External::FromDependencyOf(package, package_set, dependency, dependency_set) => {
let package_set = self.simplify_set(package_set, package);
@ -198,8 +202,8 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
/// Simplest case, we just combine two external incompatibilities.
fn explain_both_external(
&self,
external1: &External<PubGrubPackage, Range<Version>>,
external2: &External<PubGrubPackage, Range<Version>>,
external1: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
external2: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
) -> String {
let external = self.format_both_external(external1, external2);
@ -216,9 +220,9 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
fn explain_both_ref(
&self,
ref_id1: usize,
derived1: &Derived<PubGrubPackage, Range<Version>>,
derived1: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
ref_id2: usize,
derived2: &Derived<PubGrubPackage, Range<Version>>,
derived2: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
) -> String {
// TODO: order should be chosen to make it more logical.
@ -243,8 +247,8 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
fn explain_ref_and_external(
&self,
ref_id: usize,
derived: &Derived<PubGrubPackage, Range<Version>>,
external: &External<PubGrubPackage, Range<Version>>,
derived: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
) -> String {
// TODO: order should be chosen to make it more logical.
@ -265,7 +269,7 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
/// Add an external cause to the chain of explanations.
fn and_explain_external(
&self,
external: &External<PubGrubPackage, Range<Version>>,
external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
) -> String {
let external = self.format_external(external);
@ -282,7 +286,7 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
fn and_explain_ref(
&self,
ref_id: usize,
derived: &Derived<PubGrubPackage, Range<Version>>,
derived: &Derived<PubGrubPackage, Range<Version>, UnavailableReason>,
current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
) -> String {
let derived = self.format_terms(&derived.terms);
@ -299,8 +303,8 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
/// Add an already explained incompat to the chain of explanations.
fn and_explain_prior_and_external(
&self,
prior_external: &External<PubGrubPackage, Range<Version>>,
external: &External<PubGrubPackage, Range<Version>>,
prior_external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
external: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
current_terms: &Map<PubGrubPackage, Term<Range<Version>>>,
) -> String {
let external = self.format_both_external(prior_external, external);
@ -318,8 +322,8 @@ impl PubGrubReportFormatter<'_> {
/// Format two external incompatibilities, combining them if possible.
fn format_both_external(
&self,
external1: &External<PubGrubPackage, Range<Version>>,
external2: &External<PubGrubPackage, Range<Version>>,
external1: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
external2: &External<PubGrubPackage, Range<Version>, UnavailableReason>,
) -> String {
match (external1, external2) {
(
@ -387,7 +391,7 @@ impl PubGrubReportFormatter<'_> {
/// their requirements.
pub(crate) fn hints(
&self,
derivation_tree: &DerivationTree<PubGrubPackage, Range<Version>>,
derivation_tree: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
selector: &Option<CandidateSelector>,
index_locations: &Option<IndexLocations>,
unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
@ -404,7 +408,7 @@ impl PubGrubReportFormatter<'_> {
let mut hints = IndexSet::default();
match derivation_tree {
DerivationTree::External(external) => match external {
External::Unavailable(package, set, _) | External::NoVersions(package, set, _) => {
External::Custom(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)| {

View file

@ -66,16 +66,62 @@ mod provider;
mod reporter;
mod urls;
/// The package version is unavailable and cannot be used
/// Unlike [`PackageUnavailable`] this applies to a single version of the package
#[derive(Debug, Clone)]
/// The reason why a package or a version cannot be used.
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum UnavailableReason {
/// The entire package cannot be used.
Package(UnavailablePackage),
/// A single version cannot be used.
Version(UnavailableVersion),
}
impl Display for UnavailableReason {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Version(version) => Display::fmt(version, f),
Self::Package(package) => Display::fmt(package, f),
}
}
}
/// The package version is unavailable and cannot be used. Unlike [`PackageUnavailable`], this
/// applies to a single version of the package.
///
/// Most variant are from [`MetadataResponse`] without the error source (since we don't format
/// the source).
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum UnavailableVersion {
/// Version is incompatible because it has no usable distributions
IncompatibleDist(IncompatibleDist),
/// The wheel metadata was found, but could not be parsed.
InvalidMetadata,
/// The wheel metadata was found, but the metadata was inconsistent.
InconsistentMetadata,
/// The wheel has an invalid structure.
InvalidStructure,
/// The wheel metadata was not found in the cache and the network is not available.
Offline,
/// Forward any kind of resolver error.
ResolverError(String),
}
impl Display for UnavailableVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
UnavailableVersion::IncompatibleDist(invalid_dist) => Display::fmt(invalid_dist, f),
UnavailableVersion::InvalidMetadata => f.write_str("has invalid metadata"),
UnavailableVersion::InconsistentMetadata => f.write_str("has inconsistent metadata"),
UnavailableVersion::InvalidStructure => f.write_str("has an invalid package format"),
UnavailableVersion::Offline => f.write_str(
"network connectivity is disabled, but the metadata wasn't found in the cache",
),
UnavailableVersion::ResolverError(err) => f.write_str(err),
}
}
}
/// The package is unavailable and cannot be used.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
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`).
NoIndex,
@ -89,6 +135,24 @@ pub(crate) enum UnavailablePackage {
InvalidStructure(String),
}
impl UnavailablePackage {
pub(crate) fn as_str(&self) -> &'static str {
match self {
UnavailablePackage::NoIndex => "was not found in the provided package locations",
UnavailablePackage::Offline => "was not found in the cache",
UnavailablePackage::NotFound => "was not found in the package registry",
UnavailablePackage::InvalidMetadata(_) => "has invalid metadata",
UnavailablePackage::InvalidStructure(_) => "has an invalid package format",
}
}
}
impl Display for UnavailablePackage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// The package is unavailable at specific versions.
#[derive(Debug, Clone)]
pub(crate) enum IncompletePackage {
@ -359,84 +423,61 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
.term_intersection_for_package(&next)
.expect("a package was chosen but we don't have a term.");
let reason = {
if let PubGrubPackage::Package(ref package_name, _, _) = next {
// Check if the decision was due to the package being unavailable
self.unavailable_packages
.borrow()
.get(package_name)
.map(|entry| match *entry {
UnavailablePackage::NoIndex => {
"was not found in the provided package locations"
}
UnavailablePackage::Offline => "was not found in the cache",
UnavailablePackage::NotFound => {
"was not found in the package registry"
}
UnavailablePackage::InvalidMetadata(_) => {
"was found, but the metadata could not be parsed"
}
UnavailablePackage::InvalidStructure(_) => {
"was found, but has an invalid format"
}
})
} else {
None
// Check if the decision was due to the package being unavailable
if let PubGrubPackage::Package(ref package_name, _, _) = next {
if let Some(entry) = self.unavailable_packages.borrow().get(package_name) {
state.add_incompatibility(Incompatibility::custom_term(
next.clone(),
term_intersection.clone(),
UnavailableReason::Package(entry.clone()),
));
continue;
}
};
}
let inc = Incompatibility::no_versions(
state.add_incompatibility(Incompatibility::no_versions(
next.clone(),
term_intersection.clone(),
reason.map(ToString::to_string),
);
state.add_incompatibility(inc);
));
continue;
}
Some(version) => version,
};
let version = match version {
ResolverVersion::Available(version) => version,
ResolverVersion::Unavailable(version, unavailable) => {
let reason = match unavailable {
// Incompatible requires-python versions are special in that we track
// them as incompatible dependencies instead of marking the package version
// as unavailable directly
UnavailableVersion::IncompatibleDist(
IncompatibleDist::Source(IncompatibleSource::RequiresPython(
requires_python,
))
| IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(
requires_python,
)),
) => {
let python_version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
})?;
ResolverVersion::Unavailable(version, reason) => {
// Incompatible requires-python versions are special in that we track
// them as incompatible dependencies instead of marking the package version
// as unavailable directly
if let UnavailableVersion::IncompatibleDist(
IncompatibleDist::Source(IncompatibleSource::RequiresPython(
requires_python,
))
| IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(requires_python)),
) = reason
{
let python_version = requires_python
.iter()
.map(PubGrubSpecifier::try_from)
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
})?;
let package = &next;
for kind in [PubGrubPython::Installed, PubGrubPython::Target] {
state.add_incompatibility(Incompatibility::from_dependency(
package.clone(),
Range::singleton(version.clone()),
(PubGrubPackage::Python(kind), python_version.clone()),
));
}
state.partial_solution.add_decision(next.clone(), version);
continue;
}
UnavailableVersion::IncompatibleDist(incompatibility) => {
incompatibility.to_string()
let package = &next;
for kind in [PubGrubPython::Installed, PubGrubPython::Target] {
state.add_incompatibility(Incompatibility::from_dependency(
package.clone(),
Range::singleton(version.clone()),
(PubGrubPackage::Python(kind), python_version.clone()),
));
}
state.partial_solution.add_decision(next.clone(), version);
continue;
};
state.add_incompatibility(Incompatibility::unavailable(
state.add_incompatibility(Incompatibility::custom_version(
next.clone(),
version.clone(),
reason,
UnavailableReason::Version(reason),
));
continue;
}
@ -467,10 +508,10 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
.await?
{
Dependencies::Unavailable(reason) => {
state.add_incompatibility(Incompatibility::unavailable(
state.add_incompatibility(Incompatibility::custom_version(
package.clone(),
version.clone(),
reason.clone(),
UnavailableReason::Version(reason),
));
continue;
}
@ -816,7 +857,9 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
let mut constraints = match constraints {
Ok(constraints) => constraints,
Err(err) => {
return Ok(Dependencies::Unavailable(uncapitalize(err.to_string())));
return Ok(Dependencies::Unavailable(
UnavailableVersion::ResolverError(uncapitalize(err.to_string())),
));
}
};
@ -939,9 +982,9 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
false,
"Dependencies were requested for a package that is not available"
);
return Ok(Dependencies::Unavailable(
"The package is unavailable".to_string(),
));
return Err(ResolveError::Failure(format!(
"The package is unavailable: {package_name}"
)));
}
// Wait for the metadata to be available.
@ -962,10 +1005,7 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
.or_default()
.borrow_mut()
.insert(version.clone(), IncompletePackage::Offline);
return Ok(Dependencies::Unavailable(
"network connectivity is disabled, but the metadata wasn't found in the cache"
.to_string(),
));
return Ok(Dependencies::Unavailable(UnavailableVersion::Offline));
}
MetadataResponse::InvalidMetadata(err) => {
warn!("Unable to extract metadata for {package_name}: {err}");
@ -979,7 +1019,7 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
IncompletePackage::InvalidMetadata(err.to_string()),
);
return Ok(Dependencies::Unavailable(
"the package metadata could not be parsed".to_string(),
UnavailableVersion::InvalidMetadata,
));
}
MetadataResponse::InconsistentMetadata(err) => {
@ -994,7 +1034,7 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
IncompletePackage::InconsistentMetadata(err.to_string()),
);
return Ok(Dependencies::Unavailable(
"the package metadata was inconsistent".to_string(),
UnavailableVersion::InconsistentMetadata,
));
}
MetadataResponse::InvalidStructure(err) => {
@ -1009,7 +1049,7 @@ impl<'a, Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvide
IncompletePackage::InvalidStructure(err.to_string()),
);
return Ok(Dependencies::Unavailable(
"the package has an invalid format".to_string(),
UnavailableVersion::InvalidStructure,
));
}
};
@ -1342,7 +1382,7 @@ enum Response {
#[derive(Clone)]
enum Dependencies {
/// Package dependencies are not available.
Unavailable(String),
Unavailable(UnavailableVersion),
/// Container for all available package versions.
Available(Vec<(PubGrubPackage, Range<Version>)>),
}