From 420333a40ee01084f5b9ed1469e8410f3f103e94 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jun 2024 15:17:36 -0400 Subject: [PATCH] Avoid 'are incompatible' for singular bounded versions (#4003) ## Summary Not sure if this is worth the complexity, but it does read better. --- crates/uv-resolver/src/pubgrub/report.rs | 44 ++++++++++++++++++++---- crates/uv/tests/lock.rs | 2 +- crates/uv/tests/pip_install_scenarios.rs | 6 ++-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index aaa742158e24..67e1815f97dc 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -194,7 +194,15 @@ impl ReportFormatter, UnavailableReason> _ => (), } } - result.push_str(" are incompatible"); + if let [(p, t)] = slice { + if PackageTerm::new(p, t).plural() { + result.push_str(" are incompatible"); + } else { + result.push_str(" is incompatible"); + } + } else { + result.push_str(" are incompatible"); + } result } } @@ -749,13 +757,28 @@ impl std::fmt::Display for PackageTerm<'_> { } impl PackageTerm<'_> { + /// Create a new [`PackageTerm`] from a [`PubGrubPackage`] and a [`Term`]. fn new<'a>(package: &'a PubGrubPackage, term: &'a Term>) -> PackageTerm<'a> { PackageTerm { package, term } } + + /// Returns `true` if the predicate following this package term should be singular or plural. + fn plural(&self) -> bool { + match self.term { + Term::Positive(set) => PackageRange::compatibility(self.package, set).plural(), + Term::Negative(set) => { + if set.as_singleton().is_some() { + false + } else { + PackageRange::compatibility(self.package, &set.complement()).plural() + } + } + } + } } /// The kind of version ranges being displayed in [`PackageRange`] -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PackageRangeKind { Dependency, Compatibility, @@ -775,12 +798,19 @@ impl PackageRange<'_> { /// be singular or plural e.g. if false use " depends on <...>" and /// if true use " depend on <...>" fn plural(&self) -> bool { - if self.range.is_empty() { - false + let mut segments = self.range.iter(); + if let Some(segment) = segments.next() { + // A single unbounded compatibility segment is always plural ("all versions of"). + if self.kind == PackageRangeKind::Compatibility { + if matches!(segment, (Bound::Unbounded, Bound::Unbounded)) { + return true; + } + } + // Otherwise, multiple segments are always plural. + segments.next().is_some() } else { - let segments: Vec<_> = self.range.iter().collect(); - // "all versions of" is the only plural case - matches!(segments.as_slice(), [(Bound::Unbounded, Bound::Unbounded)]) + // An empty range is always singular. + false } } } diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 40e47aeda32d..a49cbdce2f49 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -991,7 +991,7 @@ fn lock_requires_python() -> Result<()> { ----- stderr ----- warning: `uv lock` is experimental and may change without warning. × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (>=3.7) does not satisfy Python>=3.8 and the requested Python version (>=3.7) does not satisfy Python>=3.7.9,<3.8, we can conclude that Python>=3.7.9 are incompatible. + ╰─▶ Because the requested Python version (>=3.7) does not satisfy Python>=3.8 and the requested Python version (>=3.7) does not satisfy Python>=3.7.9,<3.8, we can conclude that Python>=3.7.9 is incompatible. And because pygls>=1.1.0,<=1.2.1 depends on Python>=3.7.9,<4 and only pygls<=1.3.0 is available, we can conclude that any of: pygls>=1.1.0,<1.3.0 pygls>1.3.0 diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 3d55f3d00c9c..26410373c948 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -359,7 +359,7 @@ fn excluded_only_compatible_version() { And because package-a==3.0.0 depends on package-b==3.0.0, we can conclude that any of: package-a<2.0.0 package-a>2.0.0 - depends on one of: + depend on one of: package-b==1.0.0 package-b==3.0.0 @@ -4011,14 +4011,14 @@ fn python_greater_than_current_excluded() { Python>=3.10,<3.11 Python>=3.12 are incompatible. - And because the current Python version (3.9.[X]) 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.[X]) does not satisfy Python>=3.11,<3.12, we can conclude that Python>=3.10 is incompatible. And because package-a==2.0.0 depends on Python>=3.10 and only the following versions of package-a are available: package-a<=2.0.0 package-a==3.0.0 package-a==4.0.0 we can conclude that package-a>=2.0.0,<3.0.0 cannot be used. (1) - Because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that Python>=3.11 are incompatible. + Because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that Python>=3.11 is incompatible. And because package-a==3.0.0 depends on Python>=3.11, we can conclude that package-a==3.0.0 cannot be used. And because we know from (1) that package-a>=2.0.0,<3.0.0 cannot be used, we can conclude that package-a>=2.0.0,<4.0.0 cannot be used. (2)