diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index f886802d4..99c57dc42 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -96,6 +96,10 @@ pub struct PythonDownloadRequest { arch: Option, os: Option, libc: Option, + + /// Whether to allow pre-releases or not. If not set, defaults to true if [`Self::version`] is + /// not None, and false otherwise. + prereleases: Option, } impl PythonDownloadRequest { @@ -105,6 +109,7 @@ impl PythonDownloadRequest { arch: Option, os: Option, libc: Option, + prereleases: Option, ) -> Self { Self { version, @@ -112,6 +117,7 @@ impl PythonDownloadRequest { arch, os, libc, + prereleases, } } @@ -145,6 +151,12 @@ impl PythonDownloadRequest { self } + #[must_use] + pub fn with_prereleases(mut self, prereleases: bool) -> Self { + self.prereleases = Some(prereleases); + self + } + /// Construct a new [`PythonDownloadRequest`] from a [`PythonRequest`] if possible. /// /// Returns [`None`] if the request kind is not compatible with a download, e.g., it is @@ -196,6 +208,7 @@ impl PythonDownloadRequest { Some(Arch::from_env()), Some(Os::from_env()), Some(Libc::from_env()?), + None, )) } @@ -252,20 +265,22 @@ impl PythonDownloadRequest { return false; } } + // If we don't allow pre-releases, don't match a key with a pre-release tag + if !self.allows_prereleases() && !key.prerelease.is_empty() { + return false; + } true } /// Whether this request is satisfied by a Python download. - /// - /// Note that unlike [`Self::satisfied_by_key`], this method will not match a pre-release - /// unless a version is included in the request. pub fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool { - if self.version.is_none() && !download.key().prerelease.is_empty() { - return false; - } self.satisfied_by_key(download.key()) } + pub fn allows_prereleases(&self) -> bool { + self.prereleases.unwrap_or_else(|| self.version.is_some()) + } + pub fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool { if let Some(version) = self.version() { if !version.matches_interpreter(interpreter) { @@ -375,7 +390,7 @@ impl FromStr for PythonDownloadRequest { return Err(Error::TooManyParts(s.to_string())); } - Ok(Self::new(version, implementation, arch, os, libc)) + Ok(Self::new(version, implementation, arch, os, libc, None)) } } diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index e1dc8eb89..b8f089cf3 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -50,7 +50,9 @@ pub(crate) async fn list( None } } - }; + } + // Include pre-release versions + .map(|request| request.with_prereleases(true)); let downloads = download_request .as_ref() diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index 934331bd3..9e66b0460 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -75,6 +75,8 @@ async fn do_uninstall( anyhow::anyhow!("Cannot uninstall managed Python for request: {request}") }) }) + // Always include pre-releases in uninstalls + .map(|result| result.map(|request| request.with_prereleases(true))) .collect::>>()?; let installed_installations: Vec<_> = installations.find_all()?.collect();