From fb7d5361b080e449e54fb76f695129e2298ad0fe Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 9 Oct 2025 10:29:58 -0500 Subject: [PATCH] Fix handling of Python requests with pre-releases in ranges --- crates/uv-python/src/discovery.rs | 26 +++++++- crates/uv/tests/it/python_find.rs | 98 +++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 5fe1bbb85..d741a3e4e 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -2,6 +2,7 @@ use itertools::{Either, Itertools}; use regex::Regex; use rustc_hash::{FxBuildHasher, FxHashSet}; use same_file::is_same_file; +use std::borrow::Cow; use std::env::consts::EXE_SUFFIX; use std::fmt::{self, Debug, Formatter}; use std::{env, io, iter}; @@ -2692,7 +2693,16 @@ impl VersionRequest { && variant.matches_interpreter(interpreter) } Self::Range(specifiers, variant) => { - let version = interpreter.python_version().only_release(); + // If the specifier contains pre-releases, use the full version for comparison. + // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`. + let version = if specifiers + .iter() + .any(uv_pep440::VersionSpecifier::any_prerelease) + { + Cow::Borrowed(interpreter.python_version()) + } else { + Cow::Owned(interpreter.python_version().only_release()) + }; specifiers.contains(&version) && variant.matches_interpreter(interpreter) } Self::MajorMinorPrerelease(major, minor, prerelease, variant) => { @@ -2725,7 +2735,19 @@ impl VersionRequest { (version.major(), version.minor(), version.patch()) == (*major, *minor, Some(*patch)) } - Self::Range(specifiers, _) => specifiers.contains(&version.version.only_release()), + Self::Range(specifiers, _) => { + // If the specifier contains pre-releases, use the full version for comparison. + // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`. + let version = if specifiers + .iter() + .any(uv_pep440::VersionSpecifier::any_prerelease) + { + Cow::Borrowed(&version.version) + } else { + Cow::Owned(version.version.only_release()) + }; + specifiers.contains(&version) + } Self::MajorMinorPrerelease(major, minor, prerelease, _) => { (version.major(), version.minor(), version.pre()) == (*major, *minor, Some(*prerelease)) diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 0b363931e..29e56d005 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -1335,3 +1335,101 @@ fn python_find_freethreaded_314() { ----- stderr ----- "); } + +#[test] +fn python_find_prerelease_version_specifiers() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_python_sources() + .with_managed_python_dirs() + .with_python_download_cache() + .with_filtered_python_install_bin() + .with_filtered_python_names() + .with_filtered_exe_suffix(); + + context.python_install().arg("3.14.0rc2").assert().success(); + context.python_install().arg("3.14.0rc3").assert().success(); + + // `>=3.14` should allow pre-release versions + uv_snapshot!(context.filters(), context.python_find().arg(">=3.14"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // `>3.14rc2` should not match rc2 + uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc2"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // `>3.14rc3` should not match rc3 + uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc3"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for Python >3.14.0rc3 in [PYTHON SOURCES] + "); + + // `>=3.14.0rc3` should match rc3 + uv_snapshot!(context.filters(), context.python_find().arg(">=3.14.0rc3"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // `<3.14.0rc3` should match rc2 + uv_snapshot!(context.filters(), context.python_find().arg("<3.14.0rc3"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0rc2-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // `<=3.14.0rc3` should match rc3 + uv_snapshot!(context.filters(), context.python_find().arg("<=3.14.0rc3"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // Install the stable version + context.python_install().arg("3.14.0").assert().success(); + + // `>=3.14` should prefer stable + uv_snapshot!(context.filters(), context.python_find().arg(">=3.14"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // `>3.14rc2` should prefer stable + uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc2"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); +}