From 9a7a7580a2fcfa8c7e8bcd543f38a47e3caf9eec Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 11 Sep 2024 14:17:41 -0400 Subject: [PATCH] Avoid selecting prerelease Python installations without opt-in Similar to our sementics for packages with pre-release versions, we will prefer non-prerelease versions unless there are only prerelease versions available. --- crates/uv-python/src/discovery.rs | 86 ++++++++++++++++++++++++++----- docs/concepts/python-versions.md | 11 ++++ 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 84ba940b3fc39..273abfa3d4a8e 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -10,7 +10,7 @@ use thiserror::Error; use tracing::{debug, instrument, trace}; use which::{which, which_all}; -use pep440_rs::{Version, VersionSpecifiers}; +use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use uv_cache::Cache; use uv_fs::Simplified; use uv_warnings::warn_user_once; @@ -877,19 +877,43 @@ pub(crate) fn find_python_installation( preference: PythonPreference, cache: &Cache, ) -> Result { - let mut installations = find_python_installations(request, environments, preference, cache); - if let Some(result) = installations.find(|result| { - // Return the first critical discovery error or result - result.as_ref().err().map_or(true, Error::is_critical) - }) { - result - } else { - Ok(FindPythonResult::Err(PythonNotFound { - request: request.clone(), - environment_preference: environments, - python_preference: preference, - })) + let installations = find_python_installations(request, environments, preference, cache); + let mut first_prerelease = None; + for result in installations { + // Iterate until the first critical error or happy result + if !result.as_ref().err().map_or(true, Error::is_critical) { + continue; + } + + // If it's an error, we're done. + let Ok(Ok(ref installation)) = result else { + return result; + }; + + // If it's a pre-release, and pre-releases aren't allowed skip it but store it for later + if installation.python_version().pre().is_some() + && !request.allows_prereleases() + && !installation.source.allows_prereleases() + { + debug!("Skipping pre-release {}", installation.key()); + first_prerelease = Some(installation.clone()); + continue; + } + + // If we didn't skip it, this is the installation to use + return result; } + + // If we only found pre-releases, they're implicitly allowed and we should return the first one + if let Some(installation) = first_prerelease { + return Ok(Ok(installation)); + } + + Ok(FindPythonResult::Err(PythonNotFound { + request: request.clone(), + environment_preference: environments, + python_preference: preference, + })) } /// Find the best-matching Python installation. @@ -1296,6 +1320,17 @@ impl PythonRequest { } } + pub(crate) fn allows_prereleases(&self) -> bool { + match self { + Self::Any => false, + Self::Version(version) => version.allows_prereleases(), + Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true, + Self::Implementation(_) => false, + Self::ImplementationVersion(_, _) => true, + Self::Key(request) => request.allows_prereleases(), + } + } + pub(crate) fn is_explicit_system(&self) -> bool { matches!(self, Self::File(_) | Self::Directory(_)) } @@ -1320,9 +1355,21 @@ impl PythonRequest { } impl PythonSource { - pub fn is_managed(&self) -> bool { + pub fn is_managed(self) -> bool { matches!(self, Self::Managed) } + + /// Whether a pre-release Python installation from the source should be used without opt-in. + pub(crate) fn allows_prereleases(self) -> bool { + match self { + Self::Managed | Self::Registry | Self::SearchPath | Self::MicrosoftStore => false, + Self::CondaPrefix + | Self::ProvidedPath + | Self::ParentInterpreter + | Self::ActiveEnvironment + | Self::DiscoveredEnvironment => true, + } + } } impl PythonPreference { @@ -1589,6 +1636,17 @@ impl VersionRequest { Self::Range(_) => self, } } + + /// Whether this request should allow selection of pre-release versions. + pub(crate) fn allows_prereleases(&self) -> bool { + match self { + Self::Any => false, + Self::Major(_) => true, + Self::MajorMinor(..) => true, + Self::MajorMinorPatch(..) => true, + Self::Range(specifiers) => specifiers.iter().any(VersionSpecifier::any_prerelease), + } + } } impl FromStr for VersionRequest { diff --git a/docs/concepts/python-versions.md b/docs/concepts/python-versions.md index 3b1bcc0587912..23cedaceac216 100644 --- a/docs/concepts/python-versions.md +++ b/docs/concepts/python-versions.md @@ -189,6 +189,17 @@ a system Python version, uv will use the first compatible version — not the ne If a Python version cannot be found on the system, uv will check for a compatible managed Python version download. +### Python pre-releases + +Python pre-releases will not be selected by default. Python pre-releases will be used if there is no +other available installation matching the request. For example, if only a pre-release version is +available it will be used but otherwise a stable release version will be used. Similarly, if the +path to a pre-release Python executable is provided then no other Python version matches the request +and the pre-release version will be used. + +If a pre-release Python version is available and matches the request, uv will not download a stable +Python version instead. + ## Disabling automatic Python downloads By default, uv will automatically download Python versions when needed.