Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use available versions to simplify unsat error reports #547

Merged
merged 14 commits into from
Dec 12, 2023
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ once_cell = { version = "1.18.0" }
petgraph = { version = "0.6.4" }
platform-info = { version = "2.0.2" }
plist = { version = "1.6.0" }
pubgrub = { git = "https://github.com/zanieb/pubgrub", rev = "a1d584a5e506b8f0a600269373190c4a35b298d5" }
pubgrub = { git = "https://github.com/zanieb/pubgrub", rev = "8fff95d6a233a9fa4ee5ab5429033f0f0d3ddb20" }
pyo3 = { version = "0.20.0" }
pyo3-log = { version = "0.9.0"}
pyproject-toml = { version = "0.8.0" }
Expand Down
2 changes: 1 addition & 1 deletion crates/puffin-cli/src/commands/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ pub(crate) async fn pip_compile(
let resolver = Resolver::new(manifest, options, &markers, &tags, &client, &build_dispatch)
.with_reporter(ResolverReporter::from(printer));
let resolution = match resolver.resolve().await {
Err(puffin_resolver::ResolveError::PubGrub(err)) => {
Err(puffin_resolver::ResolveError::NoSolution(err)) => {
#[allow(clippy::print_stderr)]
{
let report = miette::Report::msg(format!("{err}"))
Expand Down
4 changes: 2 additions & 2 deletions crates/puffin-cli/src/commands/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,14 @@ async fn resolve(
let resolver = Resolver::new(manifest, options, markers, &tags, &client, &build_dispatch)
.with_reporter(ResolverReporter::from(printer));
let resolution = match resolver.resolve().await {
Err(puffin_resolver::ResolveError::PubGrub(err)) => {
Err(puffin_resolver::ResolveError::NoSolution(err)) => {
#[allow(clippy::print_stderr)]
{
let report = miette::Report::msg(format!("{err}"))
.context("No solution found when resolving dependencies:");
eprint!("{report:?}");
}
return Err(puffin_resolver::ResolveError::PubGrub(err).into());
return Err(puffin_resolver::ResolveError::NoSolution(err).into());
}
result => result,
}?;
Expand Down
125 changes: 106 additions & 19 deletions crates/puffin-resolver/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
use std::fmt::Formatter;

use pubgrub::range::Range;
use pubgrub::report::{DefaultStringReporter, Reporter};
use pubgrub::report::{DefaultStringReporter, DerivationTree, Reporter};
use pypi_types::IndexUrl;
use rustc_hash::FxHashMap;
use thiserror::Error;
use url::Url;

use distribution_types::{BuiltDist, PathBuiltDist, PathSourceDist, SourceDist};
use pep508_rs::Requirement;
use puffin_distribution::DistributionDatabaseError;
use puffin_normalize::PackageName;
use puffin_traits::OnceMap;

use crate::pubgrub::{PubGrubPackage, PubGrubVersion};
use crate::version_map::VersionMap;
use crate::PubGrubReportFormatter;

#[derive(Error, Debug)]
Expand All @@ -30,9 +34,6 @@ pub enum ResolveError {
#[error(transparent)]
Join(#[from] tokio::task::JoinError),

#[error(transparent)]
PubGrub(#[from] RichPubGrubError),

#[error("Package metadata name `{metadata}` does not match given name `{given}`")]
NameMismatch {
given: PackageName,
Expand Down Expand Up @@ -65,6 +66,37 @@ pub enum ResolveError {

#[error("Failed to build: {0}")]
Build(Box<PathSourceDist>, #[source] DistributionDatabaseError),

#[error(transparent)]
NoSolution(#[from] NoSolutionError),

#[error("Retrieving dependencies of {package} {version} failed")]
ErrorRetrievingDependencies {
/// Package whose dependencies we want.
package: Box<PubGrubPackage>,
/// Version of the package for which we want the dependencies.
version: Box<PubGrubVersion>,
/// Error raised by the implementer of [DependencyProvider](crate::solver::DependencyProvider).
source: Box<dyn std::error::Error + Send + Sync>,
},

#[error("{package} {version} depends on itself")]
SelfDependency {
/// Package whose dependencies we want.
package: Box<PubGrubPackage>,
/// Version of the package for which we want the dependencies.
version: Box<PubGrubVersion>,
},
Comment on lines +83 to +89
Copy link
Member

Choose a reason for hiding this comment

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

I know that is an error type in pubgrub, but fwiw we don't consider this an error (it's surprisingly popular in python packaging)

Copy link
Member Author

Choose a reason for hiding this comment

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

Great it'd probably be good to remove it then


#[error("Decision making failed")]
ErrorChoosingPackageVersion(Box<dyn std::error::Error + Send + Sync>),

#[error("We should cancel")]
ErrorInShouldCancel(Box<dyn std::error::Error + Send + Sync>),

/// Something unexpected happened.
#[error("{0}")]
Failure(String),
}

impl<T> From<futures::channel::mpsc::TrySendError<T>> for ResolveError {
Expand All @@ -73,28 +105,83 @@ impl<T> From<futures::channel::mpsc::TrySendError<T>> for ResolveError {
}
}

/// A wrapper around [`pubgrub::error::PubGrubError`] that displays a resolution failure report.
impl From<pubgrub::error::PubGrubError<PubGrubPackage, Range<PubGrubVersion>>> for ResolveError {
fn from(value: pubgrub::error::PubGrubError<PubGrubPackage, Range<PubGrubVersion>>) -> Self {
match value {
pubgrub::error::PubGrubError::ErrorChoosingPackageVersion(inner) => {
ResolveError::ErrorChoosingPackageVersion(inner)
}
pubgrub::error::PubGrubError::ErrorInShouldCancel(inner) => {
ResolveError::ErrorInShouldCancel(inner)
}
pubgrub::error::PubGrubError::ErrorRetrievingDependencies {
package,
version,
source,
} => ResolveError::ErrorRetrievingDependencies {
package: Box::new(package),
version: Box::new(version),
source,
},
pubgrub::error::PubGrubError::Failure(inner) => ResolveError::Failure(inner),
pubgrub::error::PubGrubError::NoSolution(derivation_tree) => {
ResolveError::NoSolution(NoSolutionError {
derivation_tree,
available_versions: FxHashMap::default(),
})
}
pubgrub::error::PubGrubError::SelfDependency { package, version } => {
ResolveError::SelfDependency {
package: Box::new(package),
version: Box::new(version),
}
}
}
}
}

/// A wrapper around [`pubgrub::error::PubGrubError::NoSolution`] that displays a resolution failure report.
#[derive(Debug)]
pub struct RichPubGrubError {
source: pubgrub::error::PubGrubError<PubGrubPackage, Range<PubGrubVersion>>,
pub struct NoSolutionError {
derivation_tree: DerivationTree<PubGrubPackage, Range<PubGrubVersion>>,
available_versions: FxHashMap<PubGrubPackage, Vec<PubGrubVersion>>,
}

impl std::error::Error for RichPubGrubError {}
impl std::error::Error for NoSolutionError {}

impl std::fmt::Display for RichPubGrubError {
impl std::fmt::Display for NoSolutionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let pubgrub::error::PubGrubError::NoSolution(derivation_tree) = &self.source {
let formatter = PubGrubReportFormatter;
let report = DefaultStringReporter::report_with_formatter(derivation_tree, &formatter);
write!(f, "{report}")
} else {
write!(f, "{}", self.source)
}
let formatter = PubGrubReportFormatter {
available_versions: &self.available_versions,
};
let report =
DefaultStringReporter::report_with_formatter(&self.derivation_tree, &formatter);
write!(f, "{report}")
}
}

impl From<pubgrub::error::PubGrubError<PubGrubPackage, Range<PubGrubVersion>>> for ResolveError {
fn from(value: pubgrub::error::PubGrubError<PubGrubPackage, Range<PubGrubVersion>>) -> Self {
ResolveError::PubGrub(RichPubGrubError { source: value })
impl NoSolutionError {
/// Update the available versions attached to the error using the given package version index.
///
/// Only packages used in the error's deriviation tree will be retrieved.
pub(crate) fn update_available_versions(
mut self,
package_versions: &OnceMap<PackageName, (IndexUrl, VersionMap)>,
) -> Self {
for package in self.derivation_tree.packages() {
if let PubGrubPackage::Package(name, ..) = package {
if let Some(entry) = package_versions.get(name) {
let (_, version_map) = entry.value();
self.available_versions.insert(
package.clone(),
version_map
.iter()
.map(|(version, _)| version.clone())
.collect(),
);
}
}
}
self
}
}
45 changes: 42 additions & 3 deletions crates/puffin-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,32 @@ use pubgrub::range::Range;
use pubgrub::report::{External, ReportFormatter};
use pubgrub::term::Term;
use pubgrub::type_aliases::Map;
use rustc_hash::FxHashMap;

use super::{PubGrubPackage, PubGrubVersion};

#[derive(Debug, Default)]
pub struct PubGrubReportFormatter;
#[derive(Debug)]
pub struct PubGrubReportFormatter<'a> {
/// The versions that were available for each package
pub available_versions: &'a FxHashMap<PubGrubPackage, Vec<PubGrubVersion>>,
}

impl PubGrubReportFormatter<'_> {
fn simplify_set(
&self,
set: &Range<PubGrubVersion>,
package: &PubGrubPackage,
) -> Range<PubGrubVersion> {
set.simplify(
self.available_versions
.get(package)
.unwrap_or(&vec![])
.iter(),
)
}
}

impl ReportFormatter<PubGrubPackage, Range<PubGrubVersion>> for PubGrubReportFormatter {
impl ReportFormatter<PubGrubPackage, Range<PubGrubVersion>> for PubGrubReportFormatter<'_> {
type Output = String;

fn format_external(
Expand All @@ -23,13 +42,15 @@ impl ReportFormatter<PubGrubPackage, Range<PubGrubVersion>> for PubGrubReportFor
if set == &Range::full() {
format!("there is no available version for {package}")
} else {
let set = self.simplify_set(set, package);
format!("there is no version of {package} available matching {set}")
}
}
External::UnavailableDependencies(package, set) => {
if set == &Range::full() {
format!("dependencies of {package} are unavailable")
} else {
let set = self.simplify_set(set, package);
format!("dependencies of {package} at version {set} are unavailable")
}
}
Expand All @@ -41,13 +62,15 @@ impl ReportFormatter<PubGrubPackage, Range<PubGrubVersion>> for PubGrubReportFor
if set == &Range::full() {
format!("dependencies of {package} are unusable: {reason}")
} else {
let set = self.simplify_set(set, package);
format!("dependencies of {package}{set} are unusable: {reason}",)
}
}
} else {
if set == &Range::full() {
format!("dependencies of {package} are unusable")
} else {
let set = self.simplify_set(set, package);
format!("dependencies of {package}{set} are unusable")
}
}
Expand All @@ -56,19 +79,23 @@ impl ReportFormatter<PubGrubPackage, Range<PubGrubVersion>> for PubGrubReportFor
if package_set == &Range::full() && dependency_set == &Range::full() {
format!("{package} depends on {dependency}")
} else if package_set == &Range::full() {
let dependency_set = self.simplify_set(dependency_set, package);
format!("{package} depends on {dependency}{dependency_set}")
} else if dependency_set == &Range::full() {
if matches!(package, PubGrubPackage::Root(_)) {
// Exclude the dummy version for root packages
format!("{package} depends on {dependency}")
} else {
let package_set = self.simplify_set(package_set, package);
format!("{package}{package_set} depends on {dependency}")
}
} else {
let dependency_set = self.simplify_set(dependency_set, package);
if matches!(package, PubGrubPackage::Root(_)) {
// Exclude the dummy version for root packages
format!("{package} depends on {dependency}{dependency_set}")
} else {
let package_set = self.simplify_set(package_set, package);
format!("{package}{package_set} depends on {dependency}{dependency_set}")
}
}
Expand All @@ -82,9 +109,21 @@ impl ReportFormatter<PubGrubPackage, Range<PubGrubVersion>> for PubGrubReportFor
match terms_vec.as_slice() {
[] | [(PubGrubPackage::Root(_), _)] => "version solving failed".into(),
[(package @ PubGrubPackage::Package(..), Term::Positive(range))] => {
let range = range.simplify(
self.available_versions
.get(package)
.unwrap_or(&vec![])
.iter(),
);
format!("{package}{range} is forbidden")
}
[(package @ PubGrubPackage::Package(..), Term::Negative(range))] => {
let range = range.simplify(
self.available_versions
.get(package)
.unwrap_or(&vec![])
.iter(),
);
format!("{package}{range} is mandatory")
}
[(p1, Term::Positive(r1)), (p2, Term::Negative(r2))] => self.format_external(
Expand Down
9 changes: 8 additions & 1 deletion crates/puffin-resolver/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,14 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
return Err(ResolveError::StreamTermination);
}
resolution = resolve_fut => {
resolution?
resolution.map_err(|err| {
// Add version information to improve unsat error messages
if let ResolveError::NoSolution(err) = err {
ResolveError::NoSolution(err.update_available_versions(&self.index.packages))
} else {
err
}
})?
}
};

Expand Down