Skip to content

Commit 3a53ec3

Browse files
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 #11825.
1 parent 6578885 commit 3a53ec3

File tree

2 files changed

+65
-2
lines changed

2 files changed

+65
-2
lines changed

crates/uv-python/src/discovery.rs

+21-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use which::{which, which_all};
1212
use uv_cache::Cache;
1313
use uv_fs::which::is_executable;
1414
use uv_fs::Simplified;
15-
use uv_pep440::{Prerelease, Version, VersionSpecifier, VersionSpecifiers};
15+
use uv_pep440::{
16+
release_specifiers_to_ranges, LowerBound, Prerelease, UpperBound, Version, VersionSpecifier,
17+
VersionSpecifiers,
18+
};
1619
use uv_static::EnvVars;
1720
use uv_warnings::warn_user_once;
1821

@@ -2178,7 +2181,23 @@ impl VersionRequest {
21782181
(*self_major, *self_minor) == (major, minor)
21792182
}
21802183
Self::Range(specifiers, _) => {
2181-
specifiers.contains(&Version::new([u64::from(major), u64::from(minor)]))
2184+
let range = release_specifiers_to_ranges(specifiers.clone());
2185+
let Some((lower, upper)) = range.bounding_range() else {
2186+
return true;
2187+
};
2188+
let version = Version::new([u64::from(major), u64::from(minor)]);
2189+
2190+
let lower = LowerBound::new(lower.cloned());
2191+
if !lower.major_minor().contains(&version) {
2192+
return false;
2193+
}
2194+
2195+
let upper = UpperBound::new(upper.cloned());
2196+
if !upper.major_minor().contains(&version) {
2197+
return false;
2198+
}
2199+
2200+
true
21822201
}
21832202
Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
21842203
(*self_major, *self_minor) == (major, minor)

crates/uv/tests/it/python_find.rs

+44
Original file line numberDiff line numberDiff line change
@@ -663,3 +663,47 @@ fn python_find_venv_invalid() {
663663
----- stderr -----
664664
"###);
665665
}
666+
667+
/// See: <https://github.com/astral-sh/uv/issues/11825>
668+
#[test]
669+
#[cfg(unix)]
670+
fn python_required_python_major_minor() {
671+
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
672+
673+
// Find the Python 3.11 executable.
674+
let path = &context.python_versions.first().unwrap().1;
675+
676+
// Symlink it to `python3.11`.
677+
fs_err::create_dir_all(context.temp_dir.child("child")).unwrap();
678+
std::os::unix::fs::symlink(path, context.temp_dir.child("child").join("python3.11")).unwrap();
679+
680+
// Find `python3.11`, which is `>=3.11.4`.
681+
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###"
682+
success: true
683+
exit_code: 0
684+
----- stdout -----
685+
[TEMP_DIR]/child/python3.11
686+
687+
----- stderr -----
688+
"###);
689+
690+
// Find `python3.11`, which is `>3.11.4`.
691+
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###"
692+
success: true
693+
exit_code: 0
694+
----- stdout -----
695+
[TEMP_DIR]/child/python3.11
696+
697+
----- stderr -----
698+
"###);
699+
700+
// Fail to find any matching Python interpreter.
701+
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###"
702+
success: false
703+
exit_code: 2
704+
----- stdout -----
705+
706+
----- stderr -----
707+
error: No interpreter found for Python >3.11.[X], <3.12 in virtual environments, managed installations, or search path
708+
"###);
709+
}

0 commit comments

Comments
 (0)