diff --git a/crates/pep440-rs/src/version.rs b/crates/pep440-rs/src/version.rs index bc14de343..44b971ea2 100644 --- a/crates/pep440-rs/src/version.rs +++ b/crates/pep440-rs/src/version.rs @@ -485,6 +485,14 @@ impl Version { 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 /// mutable borrow to the full type. fn make_full(&mut self) -> &mut VersionFull { diff --git a/crates/puffin-resolver/src/error.rs b/crates/puffin-resolver/src/error.rs index 71fdac554..5ab021a97 100644 --- a/crates/puffin-resolver/src/error.rs +++ b/crates/puffin-resolver/src/error.rs @@ -108,8 +108,10 @@ impl From, Infallibl pubgrub::error::PubGrubError::NoSolution(derivation_tree) => { ResolveError::NoSolution(NoSolutionError { derivation_tree, + // The following should be populated before display for the best error messages available_versions: FxHashMap::default(), selector: None, + python_requirement: None, }) } pubgrub::error::PubGrubError::SelfDependency { package, version } => { @@ -128,6 +130,14 @@ pub struct NoSolutionError { derivation_tree: DerivationTree>, available_versions: FxHashMap>, selector: Option, + python_requirement: Option, +} + +/// 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 {} @@ -137,6 +147,7 @@ impl std::fmt::Display for NoSolutionError { // Write the derivation report. let formatter = PubGrubReportFormatter { available_versions: &self.available_versions, + python_requirement: self.python_requirement.as_ref(), }; let report = DefaultStringReporter::report_with_formatter(&self.derivation_tree, &formatter); @@ -201,4 +212,17 @@ impl NoSolutionError { self.selector = Some(selector); 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 + } } diff --git a/crates/puffin-resolver/src/pubgrub/report.rs b/crates/puffin-resolver/src/pubgrub/report.rs index 5d86cd4ee..b22fcd1b4 100644 --- a/crates/puffin-resolver/src/pubgrub/report.rs +++ b/crates/puffin-resolver/src/pubgrub/report.rs @@ -12,6 +12,7 @@ use pubgrub::type_aliases::Map; use rustc_hash::{FxHashMap, FxHashSet}; use crate::candidate_selector::CandidateSelector; +use crate::error::SolutionPythonRequirement; use crate::prerelease_mode::PreReleaseStrategy; use super::PubGrubPackage; @@ -20,6 +21,9 @@ use super::PubGrubPackage; pub(crate) struct PubGrubReportFormatter<'a> { /// The versions that were available for each package pub(crate) available_versions: &'a FxHashMap>, + + /// The versions that were available for each package + pub(crate) python_requirement: Option<&'a SolutionPythonRequirement>, } impl ReportFormatter> for PubGrubReportFormatter<'_> { @@ -32,21 +36,43 @@ impl ReportFormatter> for PubGrubReportFormatter< } External::NoVersions(package, set) => { if matches!(package, PubGrubPackage::Python(_)) { - // We assume there is _only_ one available Python version as Puffin only supports - // resolution for a single Python version; if this is not the case for some reason - // we'll just fall back to the usual verbose message in production but panic in - // debug builds - if let Some([version]) = self.available_versions.get(package).map(Vec::as_slice) - { - return format!( - "{package} {version} does not satisfy {}", - PackageRange::compatibility(package, set) - ); + if let Some(python) = self.python_requirement { + if python.installed.clone().only_to_minor() == python.target { + // Simple case + return format!( + "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) + ); + } + } + } 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!( + false, + "Error reporting should always be provided with Python versions" + ) } - debug_assert!( - false, - "Unexpected value for available Python versions will degrade error message" - ); } let set = self.simplify_set(set, package); if set.as_ref() == &Range::full() { diff --git a/crates/puffin-resolver/src/resolver/mod.rs b/crates/puffin-resolver/src/resolver/mod.rs index b8ec28db0..ec88320a7 100644 --- a/crates/puffin-resolver/src/resolver/mod.rs +++ b/crates/puffin-resolver/src/resolver/mod.rs @@ -220,7 +220,12 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { // Add version information to improve unsat error messages. 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 { err } diff --git a/crates/puffin/tests/pip_compile.rs b/crates/puffin/tests/pip_compile.rs index 1e62d29fe..a56cbe694 100644 --- a/crates/puffin/tests/pip_compile.rs +++ b/crates/puffin/tests/pip_compile.rs @@ -687,9 +687,9 @@ fn compile_python_37() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because Python 3.7.17 does not satisfy Python>=3.8 and black==23.10.1 - depends on Python>=3.8, we can conclude that black==23.10.1 cannot be - used. + ╰─▶ Because the requested Python version (3.7.17, 3.12.0) does not satisfy + Python>=3.8 and black==23.10.1 depends on Python>=3.8, we can conclude + that black==23.10.1 cannot be used. And because root depends on black==23.10.1 we can conclude that the requirements are unsatisfiable. "###); diff --git a/crates/puffin/tests/pip_install_scenarios.rs b/crates/puffin/tests/pip_install_scenarios.rs index e0705d064..f89b66037 100644 --- a/crates/puffin/tests/pip_install_scenarios.rs +++ b/crates/puffin/tests/pip_install_scenarios.rs @@ -2554,7 +2554,7 @@ fn requires_python_version_does_not_exist() -> Result<()> { ----- stderr ----- × 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. "###); }); @@ -2611,7 +2611,7 @@ fn requires_python_version_less_than_current() -> Result<()> { ----- stderr ----- × 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. "###); }); @@ -2668,7 +2668,7 @@ fn requires_python_version_greater_than_current() -> Result<()> { ----- stderr ----- × 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. "###); }); @@ -2876,22 +2876,22 @@ fn requires_python_version_greater_than_current_excluded() -> Result<()> { ----- stderr ----- × 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.12 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: albatross>2.0.0,<3.0.0 albatross>3.0.0,<4.0.0 albatross>4.0.0 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 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 root depends on albatross>=2.0.0 we can conclude that the requirements are unsatisfiable. "###);