diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 9bfef91dbf698..ce09933cf7c41 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -2294,9 +2294,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 58ec9792c4920..95fe84995d113 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -485,13 +485,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 13e1b20c6d6f7..4fa0c43f525aa 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -205,10 +205,10 @@ impl TestContext { self.filters .push(("python.exe".to_string(), "python".to_string())); } else { - self.filters - .push((r"python\d".to_string(), "python".to_string())); self.filters .push((r"python\d.\d\d".to_string(), "python".to_string())); + self.filters + .push((r"python\d".to_string(), "python".to_string())); } self } @@ -224,6 +224,25 @@ impl TestContext { self } + /// Add extra standard filtering for Python installation `bin/` directories, which are not + /// present on Windows but are on Unix. See [`TestContext::with_filtered_virtualenv_bin`] for + /// the virtual environment equivalent. + #[must_use] + pub fn with_filtered_python_install_bin(mut self) -> Self { + if cfg!(unix) { + self.filters.push(( + r"[\\/]bin/python".to_string(), + "/[INSTALL-BIN]/python".to_string(), + )); + } else { + self.filters.push(( + r"[\\/]python".to_string(), + "/[INSTALL-BIN]/python".to_string(), + )); + } + self + } + /// Add extra filtering for ` -> ` symlink display for Python versions in the test /// context, e.g., for use in `uv python list`. #[must_use] diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 9fec9cdbfd05b..6f69ff3d21f07 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1201,3 +1201,76 @@ 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() + .with_filtered_python_names() + .with_filtered_python_install_bin(); + + // 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]/[INSTALL-BIN]/python + + ----- 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]/[INSTALL-BIN]/python + + ----- 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.3 in [TIME] + + cpython-3.13.3-[PLATFORM] + "); + + uv_snapshot!(context.filters(), context.python_find().arg("3"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.13.3-[PLATFORM]/[INSTALL-BIN]/python + + ----- stderr ----- + "); +}