Attach Python requirements to no solution errors to improve messaging

This commit is contained in:
Zanie 2024-01-19 13:43:13 -06:00
parent a0c116444d
commit eeadecdede
6 changed files with 88 additions and 25 deletions

View File

@ -485,6 +485,14 @@ impl Version {
self self
} }
/// Drop all components of the version except for the major and minor versions
/// For Python version matching, where we may only compare up to the minor version.
#[inline]
pub fn only_to_minor(mut self) -> Version {
self = Version::new(self.release().iter().take(2));
self
}
/// Convert this version to a "full" representation in-place and return a /// Convert this version to a "full" representation in-place and return a
/// mutable borrow to the full type. /// mutable borrow to the full type.
fn make_full(&mut self) -> &mut VersionFull { fn make_full(&mut self) -> &mut VersionFull {

View File

@ -108,8 +108,10 @@ impl From<pubgrub::error::PubGrubError<PubGrubPackage, Range<Version>, Infallibl
pubgrub::error::PubGrubError::NoSolution(derivation_tree) => { pubgrub::error::PubGrubError::NoSolution(derivation_tree) => {
ResolveError::NoSolution(NoSolutionError { ResolveError::NoSolution(NoSolutionError {
derivation_tree, derivation_tree,
// The following should be populated before display for the best error messages
available_versions: FxHashMap::default(), available_versions: FxHashMap::default(),
selector: None, selector: None,
python_requirement: None,
}) })
} }
pubgrub::error::PubGrubError::SelfDependency { package, version } => { pubgrub::error::PubGrubError::SelfDependency { package, version } => {
@ -128,6 +130,14 @@ pub struct NoSolutionError {
derivation_tree: DerivationTree<PubGrubPackage, Range<Version>>, derivation_tree: DerivationTree<PubGrubPackage, Range<Version>>,
available_versions: FxHashMap<PubGrubPackage, Vec<Version>>, available_versions: FxHashMap<PubGrubPackage, Vec<Version>>,
selector: Option<CandidateSelector>, selector: Option<CandidateSelector>,
python_requirement: Option<SolutionPythonRequirement>,
}
/// Derivative of [`PythonRequirement`] with owned data for error reporting.
#[derive(Debug, Clone)]
pub(crate) struct SolutionPythonRequirement {
pub(crate) installed: Version,
pub(crate) target: Version,
} }
impl std::error::Error for NoSolutionError {} impl std::error::Error for NoSolutionError {}
@ -137,6 +147,7 @@ impl std::fmt::Display for NoSolutionError {
// Write the derivation report. // Write the derivation report.
let formatter = PubGrubReportFormatter { let formatter = PubGrubReportFormatter {
available_versions: &self.available_versions, available_versions: &self.available_versions,
python_requirement: self.python_requirement.as_ref(),
}; };
let report = let report =
DefaultStringReporter::report_with_formatter(&self.derivation_tree, &formatter); DefaultStringReporter::report_with_formatter(&self.derivation_tree, &formatter);
@ -201,4 +212,17 @@ impl NoSolutionError {
self.selector = Some(selector); self.selector = Some(selector);
self self
} }
/// Update the Python requirements attached to the error.
#[must_use]
pub(crate) fn with_python_requirement(
mut self,
python_requirement: &PythonRequirement,
) -> Self {
self.python_requirement = Some(SolutionPythonRequirement {
target: python_requirement.target().clone(),
installed: python_requirement.installed().clone(),
});
self
}
} }

View File

@ -12,6 +12,7 @@ use pubgrub::type_aliases::Map;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use crate::candidate_selector::CandidateSelector; use crate::candidate_selector::CandidateSelector;
use crate::error::SolutionPythonRequirement;
use crate::prerelease_mode::PreReleaseStrategy; use crate::prerelease_mode::PreReleaseStrategy;
use super::PubGrubPackage; use super::PubGrubPackage;
@ -20,6 +21,9 @@ use super::PubGrubPackage;
pub(crate) struct PubGrubReportFormatter<'a> { pub(crate) struct PubGrubReportFormatter<'a> {
/// The versions that were available for each package /// The versions that were available for each package
pub(crate) available_versions: &'a FxHashMap<PubGrubPackage, Vec<Version>>, pub(crate) available_versions: &'a FxHashMap<PubGrubPackage, Vec<Version>>,
/// The versions that were available for each package
pub(crate) python_requirement: Option<&'a SolutionPythonRequirement>,
} }
impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<'_> { impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<'_> {
@ -32,21 +36,43 @@ impl ReportFormatter<PubGrubPackage, Range<Version>> for PubGrubReportFormatter<
} }
External::NoVersions(package, set) => { External::NoVersions(package, set) => {
if matches!(package, PubGrubPackage::Python(_)) { if matches!(package, PubGrubPackage::Python(_)) {
// We assume there is _only_ one available Python version as Puffin only supports if let Some(python) = self.python_requirement {
// resolution for a single Python version; if this is not the case for some reason if python.installed.clone().only_to_minor() == python.target {
// we'll just fall back to the usual verbose message in production but panic in // Simple case
// debug builds
if let Some([version]) = self.available_versions.get(package).map(Vec::as_slice)
{
return format!( return format!(
"{package} {version} does not satisfy {}", "the current {package} version ({}) does not satisfy {}",
python.target,
PackageRange::compatibility(package, set)
);
} else {
// Complex case, the target was provided and differs from the installed one
if !set.contains(&python.target) {
return format!(
"the requested {package} version ({}, {}) does not satisfy {}",
python.target,
python.installed,
PackageRange::compatibility(package, set)
);
} else {
debug_assert!(
!set.contains(&python.installed),
"There should not be an incompatibility where the range is satisfied by both Python requirements"
);
return format!(
"the current {package} version ({}) does not satisfy {}",
python.target,
PackageRange::compatibility(package, set) PackageRange::compatibility(package, set)
); );
} }
}
} else {
// We should always have the required Python versions, if we don't we'll fall back
// to a less helpful message in production
debug_assert!( debug_assert!(
false, false,
"Unexpected value for available Python versions will degrade error message" "Error reporting should always be provided with Python versions"
); )
}
} }
let set = self.simplify_set(set, package); let set = self.simplify_set(set, package);
if set.as_ref() == &Range::full() { if set.as_ref() == &Range::full() {

View File

@ -220,7 +220,12 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
// Add version information to improve unsat error messages. // Add version information to improve unsat error messages.
if let ResolveError::NoSolution(err) = err { if let ResolveError::NoSolution(err) = err {
ResolveError::NoSolution(err.with_available_versions(&self.python_requirement, &self.index.packages).with_selector(self.selector.clone())) ResolveError::NoSolution(
err
.with_available_versions(&self.python_requirement, &self.index.packages)
.with_selector(self.selector.clone())
.with_python_requirement(&self.python_requirement)
)
} else { } else {
err err
} }

View File

@ -687,9 +687,9 @@ fn compile_python_37() -> Result<()> {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because Python 3.7.17 does not satisfy Python>=3.8 and black==23.10.1 Because the requested Python version (3.7.17, 3.12.0) does not satisfy
depends on Python>=3.8, we can conclude that black==23.10.1 cannot be Python>=3.8 and black==23.10.1 depends on Python>=3.8, we can conclude
used. that black==23.10.1 cannot be used.
And because root depends on black==23.10.1 we can conclude that the And because root depends on black==23.10.1 we can conclude that the
requirements are unsatisfiable. requirements are unsatisfiable.
"###); "###);

View File

@ -2554,7 +2554,7 @@ fn requires_python_version_does_not_exist() -> Result<()> {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because Python 3.7 does not satisfy Python>=4.0 and albatross==1.0.0 depends on Python>=4.0, we can conclude that albatross==1.0.0 cannot be used. Because the current Python version (3.7.17) does not satisfy Python>=4.0 and albatross==1.0.0 depends on Python>=4.0, we can conclude that albatross==1.0.0 cannot be used.
And because root depends on albatross==1.0.0 we can conclude that the requirements are unsatisfiable. And because root depends on albatross==1.0.0 we can conclude that the requirements are unsatisfiable.
"###); "###);
}); });
@ -2611,7 +2611,7 @@ fn requires_python_version_less_than_current() -> Result<()> {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because Python 3.9 does not satisfy Python<=3.8 and albatross==1.0.0 depends on Python<=3.8, we can conclude that albatross==1.0.0 cannot be used. Because the current Python version (3.9.17) does not satisfy Python<=3.8 and albatross==1.0.0 depends on Python<=3.8, we can conclude that albatross==1.0.0 cannot be used.
And because root depends on albatross==1.0.0 we can conclude that the requirements are unsatisfiable. And because root depends on albatross==1.0.0 we can conclude that the requirements are unsatisfiable.
"###); "###);
}); });
@ -2668,7 +2668,7 @@ fn requires_python_version_greater_than_current() -> Result<()> {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because Python 3.9 does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used. Because the current Python version (3.9.17) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used.
And because root depends on albatross==1.0.0 we can conclude that the requirements are unsatisfiable. And because root depends on albatross==1.0.0 we can conclude that the requirements are unsatisfiable.
"###); "###);
}); });
@ -2876,22 +2876,22 @@ fn requires_python_version_greater_than_current_excluded() -> Result<()> {
----- stderr ----- ----- stderr -----
× No solution found when resolving dependencies: × No solution found when resolving dependencies:
Because Python 3.9 does not satisfy Python>=3.10,<3.11 and Python 3.9 does not satisfy Python>=3.12, we can conclude that any of: Because the current Python version (3.9.17) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9.17) does not satisfy Python>=3.12, we can conclude that any of:
Python>=3.10,<3.11 Python>=3.10,<3.11
Python>=3.12 Python>=3.12
are incompatible. are incompatible.
And because Python 3.9 does not satisfy Python>=3.11,<3.12 we can conclude that Python>=3.10 are incompatible. And because the current Python version (3.9.17) does not satisfy Python>=3.11,<3.12 we can conclude that Python>=3.10 are incompatible.
And because albatross==2.0.0 depends on Python>=3.10 and there are no versions of albatross that satisfy any of: And because albatross==2.0.0 depends on Python>=3.10 and there are no versions of albatross that satisfy any of:
albatross>2.0.0,<3.0.0 albatross>2.0.0,<3.0.0
albatross>3.0.0,<4.0.0 albatross>3.0.0,<4.0.0
albatross>4.0.0 albatross>4.0.0
we can conclude that albatross>=2.0.0,<3.0.0 cannot be used. (1) we can conclude that albatross>=2.0.0,<3.0.0 cannot be used. (1)
Because Python 3.9 does not satisfy Python>=3.11,<3.12 and Python 3.9 does not satisfy Python>=3.12, we can conclude that Python>=3.11 are incompatible. Because the current Python version (3.9.17) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.17) does not satisfy Python>=3.12, we can conclude that Python>=3.11 are incompatible.
And because albatross==3.0.0 depends on Python>=3.11 we can conclude that albatross==3.0.0 cannot be used. And because albatross==3.0.0 depends on Python>=3.11 we can conclude that albatross==3.0.0 cannot be used.
And because we know from (1) that albatross>=2.0.0,<3.0.0 cannot be used, we can conclude that albatross>=2.0.0,<4.0.0 cannot be used. (2) And because we know from (1) that albatross>=2.0.0,<3.0.0 cannot be used, we can conclude that albatross>=2.0.0,<4.0.0 cannot be used. (2)
Because Python 3.9 does not satisfy Python>=3.12 and albatross==4.0.0 depends on Python>=3.12, we can conclude that albatross==4.0.0 cannot be used. Because the current Python version (3.9.17) does not satisfy Python>=3.12 and albatross==4.0.0 depends on Python>=3.12, we can conclude that albatross==4.0.0 cannot be used.
And because we know from (2) that albatross>=2.0.0,<4.0.0 cannot be used, we can conclude that albatross>=2.0.0 cannot be used. And because we know from (2) that albatross>=2.0.0,<4.0.0 cannot be used, we can conclude that albatross>=2.0.0 cannot be used.
And because root depends on albatross>=2.0.0 we can conclude that the requirements are unsatisfiable. And because root depends on albatross>=2.0.0 we can conclude that the requirements are unsatisfiable.
"###); "###);