diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index cc70e6dcf..5fe1bbb85 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1945,6 +1945,24 @@ impl PythonRequest { } } + /// Check if this request includes a specific prerelease version. + pub fn includes_prerelease(&self) -> bool { + match self { + Self::Default => false, + Self::Any => false, + Self::Version(version_request) => version_request.prerelease().is_some(), + Self::Directory(..) => false, + Self::File(..) => false, + Self::ExecutableName(..) => false, + Self::Implementation(..) => false, + Self::ImplementationVersion(_, version) => version.prerelease().is_some(), + Self::Key(request) => request + .version + .as_ref() + .is_some_and(|request| request.prerelease().is_some()), + } + } + /// Check if a given interpreter satisfies the interpreter request. pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool { /// Returns `true` if the two paths refer to the same interpreter executable. @@ -2565,6 +2583,17 @@ impl VersionRequest { } } + /// Return the pre-release segment of the request, if any. + pub(crate) fn prerelease(&self) -> Option<&Prerelease> { + match self { + Self::Any | Self::Default | Self::Range(_, _) => None, + Self::Major(_, _) => None, + Self::MajorMinor(_, _, _) => None, + Self::MajorMinorPatch(_, _, _, _) => None, + Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease), + } + } + /// Check if the request is for a version supported by uv. /// /// If not, an `Err` is returned with an explanatory message. diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 0fcaa24f7..c59119fb9 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -285,9 +285,9 @@ pub(crate) async fn install( .collect::>(); if upgrade - && requests - .iter() - .any(|request| request.request.includes_patch()) + && requests.iter().any(|request| { + request.request.includes_patch() || request.request.includes_prerelease() + }) { writeln!( printer.stderr(), diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 197111ecd..d691ae24b 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1216,6 +1216,34 @@ fn python_install_freethreaded() { "); } +#[test] +fn python_upgrade_not_allowed() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix() + .with_managed_python_dirs(); + + // Request a patch upgrade + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.13.0"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: `uv python upgrade` only accepts minor versions + "); + + // Request a pre-release upgrade + uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.14rc3"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: `uv python upgrade` only accepts minor versions + "); +} + // We only support debug builds on Unix #[cfg(unix)] #[test]