From 3a53ec3c5af88a50fbb334a73880a41fd9e9b3a5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Mar 2025 15:29:39 -0800 Subject: [PATCH] Compare major-minor specifiers when filtering interpreters (#11952) ## Summary If we're looking at (e.g.) `python3.12`, and we have a `requires-python: ">=3.12.7, <3.13"`, then checking if the range includes `3.12` will return `false`. Instead, we need to look at the lower- and upper-bound major-minors of the `requires-python`. Closes https://github.com/astral-sh/uv/issues/11825. --- crates/uv-python/src/discovery.rs | 23 ++++++++++++++-- crates/uv/tests/it/python_find.rs | 44 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index b4f1ef8bd..b5a4972d6 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -12,7 +12,10 @@ use which::{which, which_all}; use uv_cache::Cache; use uv_fs::which::is_executable; use uv_fs::Simplified; -use uv_pep440::{Prerelease, Version, VersionSpecifier, VersionSpecifiers}; +use uv_pep440::{ + release_specifiers_to_ranges, LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, + VersionSpecifiers, +}; use uv_static::EnvVars; use uv_warnings::warn_user_once; @@ -2178,7 +2181,23 @@ impl VersionRequest { (*self_major, *self_minor) == (major, minor) } Self::Range(specifiers, _) => { - specifiers.contains(&Version::new([u64::from(major), u64::from(minor)])) + let range = release_specifiers_to_ranges(specifiers.clone()); + let Some((lower, upper)) = range.bounding_range() else { + return true; + }; + let version = Version::new([u64::from(major), u64::from(minor)]); + + let lower = LowerBound::new(lower.cloned()); + if !lower.major_minor().contains(&version) { + return false; + } + + let upper = UpperBound::new(upper.cloned()); + if !upper.major_minor().contains(&version) { + return false; + } + + true } Self::MajorMinorPrerelease(self_major, self_minor, _, _) => { (*self_major, *self_minor) == (major, minor) diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 944a32968..72c9f4381 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -663,3 +663,47 @@ fn python_find_venv_invalid() { ----- stderr ----- "###); } + +/// See: +#[test] +#[cfg(unix)] +fn python_required_python_major_minor() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]); + + // Find the Python 3.11 executable. + let path = &context.python_versions.first().unwrap().1; + + // Symlink it to `python3.11`. + fs_err::create_dir_all(context.temp_dir.child("child")).unwrap(); + std::os::unix::fs::symlink(path, context.temp_dir.child("child").join("python3.11")).unwrap(); + + // Find `python3.11`, which is `>=3.11.4`. + uv_snapshot!(context.filters(), context.python_find().arg(">=3.11.4, <3.12").env(EnvVars::UV_TEST_PYTHON_PATH, context.temp_dir.child("child").path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/child/python3.11 + + ----- stderr ----- + "###); + + // Find `python3.11`, which is `>3.11.4`. + uv_snapshot!(context.filters(), context.python_find().arg(">3.11.4, <3.12").env(EnvVars::UV_TEST_PYTHON_PATH, context.temp_dir.child("child").path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/child/python3.11 + + ----- stderr ----- + "###); + + // Fail to find any matching Python interpreter. + uv_snapshot!(context.filters(), context.python_find().arg(">3.11.255, <3.12").env(EnvVars::UV_TEST_PYTHON_PATH, context.temp_dir.child("child").path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for Python >3.11.[X], <3.12 in virtual environments, managed installations, or search path + "###); +}