diff --git a/crates/distribution-types/src/prioritized_distribution.rs b/crates/distribution-types/src/prioritized_distribution.rs index 258776863..36744b3a2 100644 --- a/crates/distribution-types/src/prioritized_distribution.rs +++ b/crates/distribution-types/src/prioritized_distribution.rs @@ -103,8 +103,8 @@ impl Display for IncompatibleDist { Some(_) => f.write_str("was published after the exclude newer time"), None => f.write_str("has no publish time"), }, - IncompatibleWheel::RequiresPython(python) => { - write!(f, "requires at python {python}") + IncompatibleWheel::RequiresPython(python, _) => { + write!(f, "requires Python {python}") } }, Self::Source(incompatibility) => match incompatibility { @@ -123,8 +123,8 @@ impl Display for IncompatibleDist { Some(_) => f.write_str("was published after the exclude newer time"), None => f.write_str("has no publish time"), }, - IncompatibleSource::RequiresPython(python) => { - write!(f, "requires python {python}") + IncompatibleSource::RequiresPython(python, _) => { + write!(f, "requires Python {python}") } }, Self::Unavailable => f.write_str("has no available distributions"), @@ -132,6 +132,16 @@ impl Display for IncompatibleDist { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PythonRequirementKind { + /// The installed version of Python. + Installed, + /// 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, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum WheelCompatibility { Incompatible(IncompatibleWheel), @@ -142,7 +152,7 @@ pub enum WheelCompatibility { pub enum IncompatibleWheel { ExcludeNewer(Option), Tag(IncompatibleTag), - RequiresPython(VersionSpecifiers), + RequiresPython(VersionSpecifiers, PythonRequirementKind), Yanked(Yanked), NoBinary, } @@ -156,7 +166,7 @@ pub enum SourceDistCompatibility { #[derive(Debug, PartialEq, Eq, Clone)] pub enum IncompatibleSource { ExcludeNewer(Option), - RequiresPython(VersionSpecifiers), + RequiresPython(VersionSpecifiers, PythonRequirementKind), Yanked(Yanked), NoBuild, } @@ -466,16 +476,16 @@ impl IncompatibleSource { Self::ExcludeNewer(timestamp_self) => match other { // Smaller timestamps are closer to the cut-off time Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self, - Self::NoBuild | Self::RequiresPython(_) | Self::Yanked(_) => true, + Self::NoBuild | Self::RequiresPython(_, _) | Self::Yanked(_) => true, }, - Self::RequiresPython(_) => match other { + Self::RequiresPython(_, _) => match other { Self::ExcludeNewer(_) => false, // Version specifiers cannot be reasonably compared - Self::RequiresPython(_) => false, + Self::RequiresPython(_, _) => false, Self::NoBuild | Self::Yanked(_) => true, }, Self::Yanked(_) => match other { - Self::ExcludeNewer(_) | Self::RequiresPython(_) => false, + Self::ExcludeNewer(_) | Self::RequiresPython(_, _) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), Self::NoBuild => true, @@ -497,21 +507,23 @@ impl IncompatibleWheel { timestamp_other < timestamp_self } }, - Self::NoBinary | Self::RequiresPython(_) | Self::Tag(_) | Self::Yanked(_) => true, + Self::NoBinary | Self::RequiresPython(_, _) | Self::Tag(_) | Self::Yanked(_) => { + true + } }, Self::Tag(tag_self) => match other { Self::ExcludeNewer(_) => false, Self::Tag(tag_other) => tag_other > tag_self, - Self::NoBinary | Self::RequiresPython(_) | Self::Yanked(_) => true, + Self::NoBinary | Self::RequiresPython(_, _) | Self::Yanked(_) => true, }, - Self::RequiresPython(_) => match other { + Self::RequiresPython(_, _) => match other { Self::ExcludeNewer(_) | Self::Tag(_) => false, // Version specifiers cannot be reasonably compared - Self::RequiresPython(_) => false, + Self::RequiresPython(_, _) => false, Self::NoBinary | Self::Yanked(_) => true, }, Self::Yanked(_) => match other { - Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_) => false, + Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), Self::NoBinary => true, diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 2e3765378..e3897e117 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -19,7 +19,7 @@ use crate::candidate_selector::CandidateSelector; use crate::python_requirement::PythonRequirement; use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason}; -use super::{PubGrubPackage, PubGrubPackageInner}; +use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; #[derive(Debug)] pub(crate) struct PubGrubReportFormatter<'a> { @@ -44,44 +44,29 @@ impl ReportFormatter, UnavailableReason> format!("we are solving dependencies of {package} {version}") } External::NoVersions(package, set) => { - if matches!(&**package, PubGrubPackageInner::Python(_)) { - if let Some(python) = self.python_requirement { - if python.target() == python.installed() { - // Simple case, the installed version is the same as the target version - 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" + if let Some(python) = self.python_requirement { + if matches!( + &**package, + PubGrubPackageInner::Python(PubGrubPython::Target) + ) { + return format!( + "the requested {package} version ({}) does not satisfy {}", + python.target(), + PackageRange::compatibility(package, set) ); + } + if matches!( + &**package, + PubGrubPackageInner::Python(PubGrubPython::Installed) + ) { 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() { diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 873238871..ecc4c7467 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -20,7 +20,8 @@ use tracing::{debug, enabled, instrument, trace, warn, Level}; use distribution_types::{ BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel, - InstalledDist, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, + InstalledDist, PythonRequirementKind, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist, + VersionOrUrlRef, }; pub(crate) use locals::Locals; use pep440_rs::{Version, MIN_VERSION}; @@ -406,9 +407,11 @@ impl ResolverState ResolverState { + PubGrubPython::Installed + } + PythonRequirementKind::Target => { + PubGrubPython::Target + } + }, + )), + python_version.clone(), ), - ); - } + )); state .pubgrub .partial_solution @@ -722,12 +732,29 @@ impl ResolverState