Add prerelease compatibility check (#8020)

## Summary

Closes #7977. Makes `PythonDownloadRequest` account for the prerelease
part if allowed. Also stores the prerelease in `PythonInstallationKey`
directly as a `Prerelease` rather than a string.

## Test Plan

Correctly picks the relevant prerelease (rather than picking the most
recent one):
```
λ cargo run python install 3.13.0rc2
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/uv python install 3.13.0rc2`
Searching for Python versions matching: Python 3.13rc2
cpython-3.13.0rc2-macos-aarch64-none ------------------------------ 457.81 KiB/14.73 MiB                                                                                                                    ^C

λ cargo run python install 3.13.0rc3                 
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/uv python install 3.13.0rc3`
Searching for Python versions matching: Python 3.13rc3
Found existing installation for Python 3.13rc3: cpython-3.13.0rc3-macos-aarch64-none
```
This commit is contained in:
trag1c 2024-10-08 23:20:58 +02:00 committed by GitHub
parent cfaa834dee
commit 37273cb4bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 794 additions and 768 deletions

View file

@ -1869,7 +1869,13 @@ impl VersionRequest {
}
}
pub(crate) fn matches_major_minor_patch(&self, major: u8, minor: u8, patch: u8) -> bool {
pub(crate) fn matches_major_minor_patch_prerelease(
&self,
major: u8,
minor: u8,
patch: u8,
prerelease: Option<Prerelease>,
) -> bool {
match self {
Self::Any | Self::Default => true,
Self::Major(self_major, _) => *self_major == major,
@ -1879,14 +1885,14 @@ impl VersionRequest {
Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
(*self_major, *self_minor, *self_patch) == (major, minor, patch)
}
Self::Range(specifiers, _) => specifiers.contains(&Version::new([
u64::from(major),
u64::from(minor),
u64::from(patch),
])),
Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
Self::Range(specifiers, _) => specifiers.contains(
&Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
.with_pre(prerelease),
),
Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
// Pre-releases of Python versions are always for the zero patch version
(*self_major, *self_minor, 0) == (major, minor, patch)
&& prerelease.map_or(true, |pre| *self_prerelease == pre)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
// Generated with `{{generated_with}}`
// From template at `{{generated_from}}`
use std::borrow::Cow;
use uv_pep440::{Prerelease, PrereleaseKind};
pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[
{{#versions}}
@ -12,7 +12,7 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[
major: {{value.major}},
minor: {{value.minor}},
patch: {{value.patch}},
prerelease: Cow::Borrowed("{{value.prerelease}}"),
prerelease: {{value.prerelease}},
implementation: LenientImplementationName::Known(ImplementationName::{{value.name}}),
arch: Arch(target_lexicon::Architecture::{{value.arch}}),
os: Os(target_lexicon::OperatingSystem::{{value.os}}),

View file

@ -260,8 +260,17 @@ 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_some() {
return false;
}
if let Some(version) = &self.version {
if !version.matches_major_minor_patch(key.major, key.minor, key.patch) {
if !version.matches_major_minor_patch_prerelease(
key.major,
key.minor,
key.patch,
key.prerelease,
) {
return false;
}
if version.is_freethreaded() {
@ -269,10 +278,6 @@ 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
}

View file

@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
@ -6,7 +5,7 @@ use tracing::{debug, info};
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_pep440::Version;
use uv_pep440::{Prerelease, Version};
use crate::discovery::{
find_best_python_installation, find_python_installation, EnvironmentPreference, PythonRequest,
@ -224,7 +223,7 @@ pub struct PythonInstallationKey {
pub(crate) major: u8,
pub(crate) minor: u8,
pub(crate) patch: u8,
pub(crate) prerelease: Cow<'static, str>,
pub(crate) prerelease: Option<Prerelease>,
pub(crate) os: Os,
pub(crate) arch: Arch,
pub(crate) libc: Libc,
@ -236,7 +235,7 @@ impl PythonInstallationKey {
major: u8,
minor: u8,
patch: u8,
prerelease: String,
prerelease: Option<Prerelease>,
os: Os,
arch: Arch,
libc: Libc,
@ -246,7 +245,7 @@ impl PythonInstallationKey {
major,
minor,
patch,
prerelease: Cow::Owned(prerelease),
prerelease,
os,
arch,
libc,
@ -265,7 +264,7 @@ impl PythonInstallationKey {
major: version.major(),
minor: version.minor(),
patch: version.patch().unwrap_or_default(),
prerelease: Cow::Owned(version.pre().map(|pre| pre.to_string()).unwrap_or_default()),
prerelease: version.pre(),
os,
arch,
libc,
@ -279,7 +278,12 @@ impl PythonInstallationKey {
pub fn version(&self) -> PythonVersion {
PythonVersion::from_str(&format!(
"{}.{}.{}{}",
self.major, self.minor, self.patch, self.prerelease
self.major,
self.minor,
self.patch,
self.prerelease
.map(|pre| pre.to_string())
.unwrap_or_default()
))
.expect("Python installation keys must have valid Python versions")
}
@ -306,7 +310,9 @@ impl fmt::Display for PythonInstallationKey {
self.major,
self.minor,
self.patch,
self.prerelease,
self.prerelease
.map(|pre| pre.to_string())
.unwrap_or_default(),
self.os,
self.arch,
self.libc

View file

@ -157,10 +157,7 @@ impl Interpreter {
self.python_major(),
self.python_minor(),
self.python_patch(),
self.python_version()
.pre()
.map(|pre| pre.to_string())
.unwrap_or_default(),
self.python_version().pre(),
self.os(),
self.arch(),
self.libc(),

View file

@ -17,6 +17,7 @@ Usage:
import argparse
import json
import logging
import re
import subprocess
import sys
from pathlib import Path
@ -29,6 +30,7 @@ WORKSPACE_ROOT = CRATE_ROOT.parent.parent
VERSION_METADATA = CRATE_ROOT / "download-metadata.json"
TEMPLATE = CRATE_ROOT / "src" / "downloads.inc.mustache"
TARGET = TEMPLATE.with_suffix("")
PRERELEASE_PATTERN = re.compile(r"(a|b|rc)(\d+)")
def prepare_name(name: str) -> str:
@ -61,11 +63,21 @@ def prepare_arch(arch: str) -> str:
return arch.capitalize()
def prepare_prerelease(prerelease: str) -> str:
if not prerelease:
return "None"
if not (match := PRERELEASE_PATTERN.match(prerelease)):
raise ValueError(f"Invalid prerelease: {prerelease!r}")
kind, number = match.groups()
return f"Some(Prerelease {{ kind: PrereleaseKind::{kind.capitalize()}, number: {number} }})"
def prepare_value(value: dict) -> dict:
value["os"] = value["os"].title()
value["arch"] = prepare_arch(value["arch"])
value["name"] = prepare_name(value["name"])
value["libc"] = prepare_libc(value["libc"])
value["prerelease"] = prepare_prerelease(value["prerelease"])
return value