From bfecc9902ee99e602bdca64c41c59c543b3b3b24 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 7 Nov 2025 09:23:37 -0600 Subject: [PATCH] Fix inclusive constraints on available package versions in resolver errors (#16629) Closes https://github.com/astral-sh/uv/issues/16626 --- crates/uv-resolver/src/pubgrub/report.rs | 21 +++++++++++++++++++-- crates/uv/tests/it/lock_scenarios.rs | 2 +- crates/uv/tests/it/pip_compile.rs | 8 ++++---- crates/uv/tests/it/pip_install.rs | 2 +- crates/uv/tests/it/pip_install_scenarios.rs | 14 +++++++------- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 564b2fecd..f79ae4492 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -1895,6 +1895,23 @@ fn update_availability_range( range: &Range, available_versions: &BTreeSet, ) -> Range { + /// Whether a (normalized) version is contained in a set of versions. + /// + /// Unfortunately, we need to normalize the version because when we extract it from the range it + /// may have `min` or `max` set but the values in `available_versions` will never have `min` or + /// `max`. + fn version_contained_in(version: &Version, versions: &BTreeSet) -> bool { + if versions.contains(version) { + return true; + } + + // It's a little unfortunate we perform a clone here and throw away the value, but the + // performance implications during an error report seem negligible and it makes the + // calling code simpler. + let version = version.clone().with_min(None).with_max(None); + versions.contains(&version) + } + let mut new_range = Range::empty(); // Construct an available range to help guide simplification. Note this is not strictly correct, @@ -1957,13 +1974,13 @@ fn update_availability_range( // bound to avoid confusion, e.g., if the segment is `foo<=10` and the available versions // do not include `foo 10`, we should instead say `foo<10`. let lower = match lower { - Bound::Included(version) if !available_versions.contains(version) => { + Bound::Included(version) if !version_contained_in(version, available_versions) => { Bound::Excluded(version.clone()) } _ => (*lower).clone(), }; let upper = match upper { - Bound::Included(version) if !available_versions.contains(version) => { + Bound::Included(version) if !version_contained_in(version, available_versions) => { Bound::Excluded(version.clone()) } _ => (*upper).clone(), diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index a58380ae3..088fc18c3 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -783,7 +783,7 @@ fn conflict_in_fork() -> Result<()> { And because package-a==1.0.0 depends on package-b and package-c, we can conclude that package-a==1.0.0 cannot be used. And because only the following versions of package-a{sys_platform == 'os2'} are available: package-a{sys_platform == 'os2'}==1.0.0 - package-a{sys_platform == 'os2'}>2 + package-a{sys_platform == 'os2'}>=2 and your project depends on package-a{sys_platform == 'os2'}<2, we can conclude that your project's requirements are unsatisfiable. hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`. diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 8a7b9f8fb..112b1bf68 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3952,7 +3952,7 @@ fn compile_yanked_version_indirect() -> Result<()> { requirements_in.write_str("attrs>20.3.0,<21.2.0")?; uv_snapshot!(context.filters(), context.pip_compile() - .arg("requirements.in"), @r###" + .arg("requirements.in"), @r" success: false exit_code: 1 ----- stdout ----- @@ -3960,12 +3960,12 @@ fn compile_yanked_version_indirect() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only the following versions of attrs are available: - attrs<20.3.0 + attrs<=20.3.0 attrs==21.1.0 - attrs>21.2.0 + attrs>=21.2.0 and attrs==21.1.0 was yanked (reason: Installable but not importable on Python 3.4), we can conclude that attrs>20.3.0,<21.2.0 cannot be used. And because you require attrs>20.3.0,<21.2.0, we can conclude that your requirements are unsatisfiable. - "### + " ); Ok(()) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 3048f32fe..accb2aca4 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -3719,7 +3719,7 @@ fn build_prerelease_hint() -> Result<()> { × Failed to build `project @ file://[TEMP_DIR]/` ├─▶ Failed to resolve requirements from `build-system.requires` ├─▶ No solution found when resolving: `transitive-package-only-prereleases-in-range-a` - ╰─▶ Because only transitive-package-only-prereleases-in-range-b<0.1 is available and transitive-package-only-prereleases-in-range-a==0.1.0 depends on transitive-package-only-prereleases-in-range-b>0.1, we can conclude that transitive-package-only-prereleases-in-range-a==0.1.0 cannot be used. + ╰─▶ Because only transitive-package-only-prereleases-in-range-b<=0.1 is available and transitive-package-only-prereleases-in-range-a==0.1.0 depends on transitive-package-only-prereleases-in-range-b>0.1, we can conclude that transitive-package-only-prereleases-in-range-a==0.1.0 cannot be used. And because only transitive-package-only-prereleases-in-range-a==0.1.0 is available and you require transitive-package-only-prereleases-in-range-a, we can conclude that your requirements are unsatisfiable. hint: Only pre-releases of `transitive-package-only-prereleases-in-range-b` (e.g., 1.0.0a1) match these build requirements, and build environments can't enable pre-releases automatically. Add `transitive-package-only-prereleases-in-range-b>=1.0.0a1` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`. diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index 68fdd236d..b2e43fa88 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -284,7 +284,7 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: package-a==1.0.0 - package-a>2.0.0 + package-a>=2.0.0 we can conclude that package-a<2.0.0 depends on package-b==1.0.0. And because only package-a<=3.0.0 is available, we can conclude that package-a<2.0.0 depends on package-b==1.0.0. (1) @@ -387,7 +387,7 @@ fn dependency_excludes_range_of_compatible_versions() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: package-a==1.0.0 - package-a>2.0.0 + package-a>=2.0.0 we can conclude that package-a<2.0.0 depends on package-b==1.0.0. And because only package-a<=3.0.0 is available, we can conclude that package-a<2.0.0 depends on package-b==1.0.0. (1) @@ -2383,7 +2383,7 @@ fn package_only_prereleases_in_range() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only package-a<0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. + ╰─▶ Because only package-a<=0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. hint: Pre-releases are available for `package-a` in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) "); @@ -2872,7 +2872,7 @@ fn transitive_package_only_prereleases_in_range() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only package-b<0.1 is available and package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. + ╰─▶ Because only package-b<=0.1 is available and package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. hint: Pre-releases are available for `package-b` in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) @@ -2996,7 +2996,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only the following versions of package-c are available: - package-c<1.0.0 + package-c<=1.0.0 package-c>=2.0.0a5,<=2.0.0a7 package-c==2.0.0b1 package-c>=2.0.0b5 @@ -3980,7 +3980,7 @@ fn package_only_yanked_in_range() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only the following versions of package-a are available: - package-a<0.1.0 + package-a<=0.1.0 package-a==1.0.0 and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-a>0.1.0 cannot be used. And because you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. @@ -4195,7 +4195,7 @@ fn transitive_package_only_yanked_in_range() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only the following versions of package-b are available: - package-b<0.1 + package-b<=0.1 package-b==1.0.0 and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-b>0.1 cannot be used. And because package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used.