From f22e5ef69ac2f3dbdcb7d6d7c68ff6d85891e0a8 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 11 Sep 2024 15:49:33 -0500 Subject: [PATCH] Avoid selecting prerelease Python installations without opt-in (#7300) Similar to our semantics for packages with pre-release versions. We will not use prerelease versions unless there are only prerelease versions available, a specific version is requested, or the prerelease version is found in a reasonable source (active environment, explicit path, etc. but not `PATH`). For example, `uv python install 3.13 && uv run python --version` will no longer use `3.13.0rc2` unless that is the only Python version available, `--python 3.13` is used, or that's the Python version that is present in `.venv`. --- .github/workflows/ci.yml | 2 +- crates/uv-python/src/discovery.rs | 86 ++++++++++++++++++++++++++----- docs/concepts/python-versions.md | 11 ++++ 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 034c384d01ce..324652542565 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1405,7 +1405,7 @@ jobs: run: echo $(which python3.13) - name: "Validate global Python install" - run: python3.13 scripts/check_system_python.py --uv ./uv + run: python3.13 scripts/check_system_python.py --uv ./uv --python 3.13 system-test-conda: timeout-minutes: 10 diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 84ba940b3fc3..273abfa3d4a8 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 3b1bcc058791..23cedaceac21 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.