diff --git a/crates/puffin-resolver/src/error.rs b/crates/puffin-resolver/src/error.rs index 71fdac5547c4..09d8d3a97930 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,7 @@ pub struct NoSolutionError { derivation_tree: DerivationTree>, available_versions: FxHashMap>, selector: Option, + python_requirement: Option, } impl std::error::Error for NoSolutionError {} @@ -137,6 +140,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 +205,14 @@ 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(python_requirement.clone()); + self + } } diff --git a/crates/puffin-resolver/src/pubgrub/report.rs b/crates/puffin-resolver/src/pubgrub/report.rs index 9b89c803f1e4..8143030eee1c 100644 --- a/crates/puffin-resolver/src/pubgrub/report.rs +++ b/crates/puffin-resolver/src/pubgrub/report.rs @@ -13,6 +13,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::candidate_selector::CandidateSelector; use crate::prerelease_mode::PreReleaseStrategy; +use crate::python_requirement::PythonRequirement; 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 PythonRequirement>, } impl ReportFormatter> for PubGrubReportFormatter<'_> { @@ -31,6 +35,53 @@ impl ReportFormatter> for PubGrubReportFormatter< format!("we are solving dependencies of {package} {version}") } External::NoVersions(package, set) => { + if matches!(package, PubGrubPackage::Python(_)) { + if let Some(python) = self.python_requirement { + if python.target().release().iter().eq(python + .installed() + .release() + .iter() + .take(2)) + { + // Simple case, the installed version is the same as the target version + // N.B. Usually the target version does not include anything past the + // minor version mumber so we only compare to part of the installed + // version. If the target version is longer, we'll do the complex + // display instead. + return format!( + "the current {package} version ({}) does not satisfy {}", + python.target(), + PackageRange::compatibility(package, set) + ); + } + // Complex case, the target was provided and differs from the installed one + // Determine which Python version requirement was not met + if !set.contains(python.target()) { + return format!( + "the requested {package} version ({}) does not satisfy {}", + python.target(), + PackageRange::compatibility(package, set) + ); + } + // TODO(zanieb): Explain to the user why the installed version is relevant + // when they provided a target version; probably via a "hint" + 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.installed(), + PackageRange::compatibility(package, set) + ); + } + // 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" + ); + } let set = self.simplify_set(set, package); if set.as_ref() == &Range::full() { format!("there are no versions of {package}") diff --git a/crates/puffin-resolver/src/python_requirement.rs b/crates/puffin-resolver/src/python_requirement.rs index c1a26d50c017..6d8f7dadb612 100644 --- a/crates/puffin-resolver/src/python_requirement.rs +++ b/crates/puffin-resolver/src/python_requirement.rs @@ -3,30 +3,30 @@ use pep508_rs::MarkerEnvironment; use puffin_interpreter::Interpreter; #[derive(Debug, Clone)] -pub struct PythonRequirement<'a> { +pub struct PythonRequirement { /// The installed version of Python. - installed: &'a Version, + installed: Version, /// The target version of Python; that is, the version of Python for which we are resolving /// dependencies. This is typically the same as the installed version, but may be different /// when specifying an alternate Python version for the resolution. - target: &'a Version, + target: Version, } -impl<'a> PythonRequirement<'a> { - pub fn new(interpreter: &'a Interpreter, markers: &'a MarkerEnvironment) -> Self { +impl PythonRequirement { + pub fn new(interpreter: &Interpreter, markers: &MarkerEnvironment) -> Self { Self { - installed: interpreter.version(), - target: &markers.python_version.version, + installed: interpreter.version().clone(), + target: markers.python_version.version.clone(), } } /// Return the installed version of Python. - pub(crate) fn installed(&self) -> &'a Version { - self.installed + pub(crate) fn installed(&self) -> &Version { + &self.installed } /// Return the target version of Python. - pub(crate) fn target(&self) -> &'a Version { - self.target + pub(crate) fn target(&self) -> &Version { + &self.target } } diff --git a/crates/puffin-resolver/src/resolver/mod.rs b/crates/puffin-resolver/src/resolver/mod.rs index 0287dc24e022..379e2ba04e02 100644 --- a/crates/puffin-resolver/src/resolver/mod.rs +++ b/crates/puffin-resolver/src/resolver/mod.rs @@ -63,7 +63,7 @@ pub struct Resolver<'a, Provider: ResolverProvider> { overrides: Overrides, allowed_urls: AllowedUrls, markers: &'a MarkerEnvironment, - python_requirement: PythonRequirement<'a>, + python_requirement: PythonRequirement, selector: CandidateSelector, index: &'a InMemoryIndex, /// A map from [`PackageId`] to the `Requires-Python` version specifiers for that package. @@ -120,7 +120,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { manifest: Manifest, options: ResolutionOptions, markers: &'a MarkerEnvironment, - python_requirement: PythonRequirement<'a>, + python_requirement: PythonRequirement, index: &'a InMemoryIndex, provider: Provider, ) -> Self { @@ -221,7 +221,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-resolver/src/resolver/provider.rs b/crates/puffin-resolver/src/resolver/provider.rs index 3f9ac5aa900d..6f7bdb3e0734 100644 --- a/crates/puffin-resolver/src/resolver/provider.rs +++ b/crates/puffin-resolver/src/resolver/provider.rs @@ -52,7 +52,7 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext + Send + Sync> { /// These are the entries from `--find-links` that act as overrides for index responses. flat_index: &'a FlatIndex, tags: &'a Tags, - python_requirement: PythonRequirement<'a>, + python_requirement: PythonRequirement, exclude_newer: Option>, allowed_yanks: AllowedYanks, no_binary: &'a NoBinary, @@ -66,7 +66,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex fetcher: DistributionDatabase<'a, Context>, flat_index: &'a FlatIndex, tags: &'a Tags, - python_requirement: PythonRequirement<'a>, + python_requirement: PythonRequirement, exclude_newer: Option>, allowed_yanks: AllowedYanks, no_binary: &'a NoBinary, diff --git a/crates/puffin/tests/pip_compile.rs b/crates/puffin/tests/pip_compile.rs index 40d887c213f8..2e3dca6a1af5 100644 --- a/crates/puffin/tests/pip_compile.rs +++ b/crates/puffin/tests/pip_compile.rs @@ -687,8 +687,9 @@ fn compile_python_37() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only Python<3.8 is available 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) 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 you require 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 84c6dcb790cb..c121a33669fb 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 only Python<4.0 is available 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) 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 you require 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 only Python>3.8 is available 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) 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 you require 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 only Python<3.10 is available 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) 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 you require 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 there are no versions of Python that satisfy Python>=3.10,<3.11 and only Python<3.12 is available, we can conclude that any of: + ╰─▶ Because the current Python version (3.9) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9) does not satisfy Python>=3.12, we can conclude that any of: Python>=3.10,<3.11 Python>=3.12 are incompatible. - And because there are no versions of Python that satisfy Python>=3.11,<3.12 we can conclude that Python>=3.10 are incompatible. + And because the current Python version (3.9) 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 there are no versions of Python that satisfy Python>=3.11,<3.12 and only Python<3.12 is available, we can conclude that Python>=3.11 are incompatible. + Because the current Python version (3.9) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9) 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 only Python<3.12 is available 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) 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 you require albatross>=2.0.0 we can conclude that the requirements are unsatisfiable. "###);