From 1afe35aef022d377ba16222c214ba3a43576cee9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 30 Jul 2024 19:35:09 -0400 Subject: [PATCH] Prioritize forks based on upper bounds --- crates/uv-resolver/src/resolver/mod.rs | 29 ++++++++++++- crates/uv/tests/pip_compile.rs | 57 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 0236896dc04d..c4cd4ccd33fc 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -2843,7 +2843,34 @@ impl Ord for Fork { // work for the latter, but the inverse is unlikely to be true. let self_bound = requires_python_marker(&self.markers).unwrap_or_default(); let other_bound = requires_python_marker(&other.markers).unwrap_or_default(); - other_bound.cmp(&self_bound) + + other_bound.cmp(&self_bound).then_with(|| { + // If there's no difference, prioritize forks with upper bounds. We'd prefer to solve + // `numpy <= 2` before solving `numpy >= 1`, since the resolution produced by the former + // might work for the latter, but the inverse is unlikely to be true due to maximum + // version selection. (Selecting `numpy==2.0.0` would satisfy both forks, but selecting + // the latest `numpy` would not.) + let self_upper_bounds = self + .dependencies + .iter() + .filter(|dep| { + dep.version + .bounding_range() + .is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded)) + }) + .count(); + let other_upper_bounds = other + .dependencies + .iter() + .filter(|dep| { + dep.version + .bounding_range() + .is_some_and(|(_, upper)| !matches!(upper, Bound::Unbounded)) + }) + .count(); + + self_upper_bounds.cmp(&other_upper_bounds) + }) } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 63857b2556d3..c811906d6294 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -7963,6 +7963,63 @@ fn universal_no_repeated_unconditional_distributions() -> Result<()> { Ok(()) } +/// Solve for upper bounds before solving for lower bounds. A solution that satisfies `pylint < 3` +/// can also work for `pylint > 2`, but the inverse isn't true (due to maximum version selection). +#[test] +fn universal_prefer_upper_bounds() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc::indoc! {r" + pylint < 3 ; sys_platform == 'darwin' + pylint > 2 ; sys_platform != 'darwin' + "})?; + + uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() + .arg("requirements.in") + .arg("-p") + .arg("3.8") + .arg("--universal"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.8 --universal + astroid==2.15.8 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via pylint + colorama==0.4.6 ; sys_platform == 'win32' + # via pylint + dill==0.3.8 + # via pylint + isort==5.13.2 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via pylint + lazy-object-proxy==1.10.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via astroid + mccabe==0.7.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via pylint + platformdirs==4.2.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via pylint + pylint==2.17.7 + # via -r requirements.in + tomli==2.0.1 ; python_version < '3.11' + # via pylint + tomlkit==0.12.4 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via pylint + typing-extensions==4.10.0 ; python_version < '3.11' + # via + # astroid + # pylint + wrapt==1.16.0 ; (python_version < '3.11' and sys_platform == 'darwin') or (python_version < '3.11' and sys_platform != 'darwin') or (python_version >= '3.11' and sys_platform == 'darwin') or (python_version >= '3.11' and sys_platform != 'darwin') + # via astroid + + ----- stderr ----- + warning: The requested Python version 3.8 is not available; 3.12.[X] will be used to build dependencies instead. + Resolved 12 packages in [TIME] + "### + ); + + Ok(()) +} + /// Remove `python_version` markers that are always true. #[test] fn universal_unnecessary_python() -> Result<()> {