Skip to content

Commit

Permalink
Avoid selecting prerelease Python installations without opt-in
Browse files Browse the repository at this point in the history
Similar to our sementics for packages with pre-release versions, we will prefer non-prerelease versions unless there are only prerelease versions available.
  • Loading branch information
zanieb committed Sep 11, 2024
1 parent c50eb12 commit 9a7a758
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 14 deletions.
86 changes: 72 additions & 14 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -877,19 +877,43 @@ pub(crate) fn find_python_installation(
preference: PythonPreference,
cache: &Cache,
) -> Result<FindPythonResult, Error> {
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.
Expand Down Expand Up @@ -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(_))
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions docs/concepts/python-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 9a7a758

Please sign in to comment.