diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index b5a4972d69f2..1aa58c20e356 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -2275,9 +2275,9 @@ impl VersionRequest { match self { Self::Default => false, Self::Any => true, - Self::Major(..) => true, - Self::MajorMinor(..) => true, - Self::MajorMinorPatch(..) => true, + Self::Major(..) => false, + Self::MajorMinor(..) => false, + Self::MajorMinorPatch(..) => false, Self::MajorMinorPrerelease(..) => true, Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease), } diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index aa5ca64fd7f8..eca1baa9ac06 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -455,13 +455,28 @@ pub enum DownloadResult { impl ManagedPythonDownload { /// Return the first [`ManagedPythonDownload`] matching a request, if any. + /// + /// If there is no stable version matching the request, a compatible pre-release version will + /// be searched for — even if a pre-release was not explicitly requested. pub fn from_request( request: &PythonDownloadRequest, ) -> Result<&'static ManagedPythonDownload, Error> { - request - .iter_downloads() - .next() - .ok_or(Error::NoDownloadFound(request.clone())) + if let Some(download) = request.iter_downloads().next() { + return Ok(download); + } + + if !request.allows_prereleases() { + if let Some(download) = request + .clone() + .with_prereleases(true) + .iter_downloads() + .next() + { + return Ok(download); + } + } + + Err(Error::NoDownloadFound(request.clone())) } /// Iterate over all [`ManagedPythonDownload`]s. diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 7407cba7c98b..89937d92e616 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -257,7 +257,7 @@ impl TestContext { pub fn with_filtered_python_keys(mut self) -> Self { // Filter platform keys self.filters.push(( - r"((?:cpython|pypy)-\d+\.\d+(?:\.(?:\[X\]|\d+))?[a-z]?(?:\+[a-z]+)?)-[a-z0-9]+-[a-z0-9_]+-[a-z]+" + r"((?:cpython|pypy)-\d+\.\d+(?:\.(?:\[X\]|\d+))?[a-z]?(?:[a-z][0-9])?(?:\+[a-z]+)?)-[a-z0-9]+-[a-z0-9_]+-[a-z]+" .to_string(), "$1-[PLATFORM]".to_string(), )); diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index fbc945e466bf..f0de1a71f610 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1201,3 +1201,74 @@ fn python_install_patch_dylib() { ----- stderr ----- "###); } + +#[test] +fn python_install_314() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Install 3.14 + // For now, this provides test coverage of pre-release handling + uv_snapshot!(context.filters(), context.python_install().arg("3.14"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.14.0a6 in [TIME] + + cpython-3.14.0a6-[PLATFORM] + "); + + // Install a specific pre-release + uv_snapshot!(context.filters(), context.python_install().arg("3.14.0a4"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.14.0a4 in [TIME] + + cpython-3.14.0a4-[PLATFORM] + "); + + // We should be able to find this version without opt-in, because there is no stable release + // installed + uv_snapshot!(context.filters(), context.python_find().arg("3.14"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0a6-[PLATFORM]/bin/python3.14 + + ----- stderr ----- + "); + + uv_snapshot!(context.filters(), context.python_find().arg("3"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14.0a6-[PLATFORM]/bin/python3.14 + + ----- stderr ----- + "); + + // If we install a stable version, that should be preferred though + uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.2 in [TIME] + + cpython-3.13.2-[PLATFORM] + "); + + uv_snapshot!(context.filters(), context.python_find().arg("3"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.13.2-[PLATFORM]/bin/python3.13 + + ----- stderr ----- + "); +}