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 { match self {
Self::Any | Self::Default => true, Self::Any | Self::Default => true,
Self::Major(self_major, _) => *self_major == major, Self::Major(self_major, _) => *self_major == major,
@ -1879,14 +1885,14 @@ impl VersionRequest {
Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => { Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
(*self_major, *self_minor, *self_patch) == (major, minor, patch) (*self_major, *self_minor, *self_patch) == (major, minor, patch)
} }
Self::Range(specifiers, _) => specifiers.contains(&Version::new([ Self::Range(specifiers, _) => specifiers.contains(
u64::from(major), &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
u64::from(minor), .with_pre(prerelease),
u64::from(patch), ),
])), Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
// Pre-releases of Python versions are always for the zero patch version // Pre-releases of Python versions are always for the zero patch version
(*self_major, *self_minor, 0) == (major, minor, patch) (*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}}` // Generated with `{{generated_with}}`
// From template at `{{generated_from}}` // From template at `{{generated_from}}`
use std::borrow::Cow; use uv_pep440::{Prerelease, PrereleaseKind};
pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[
{{#versions}} {{#versions}}
@ -12,7 +12,7 @@ pub(crate) const PYTHON_DOWNLOADS: &[ManagedPythonDownload] = &[
major: {{value.major}}, major: {{value.major}},
minor: {{value.minor}}, minor: {{value.minor}},
patch: {{value.patch}}, patch: {{value.patch}},
prerelease: Cow::Borrowed("{{value.prerelease}}"), prerelease: {{value.prerelease}},
implementation: LenientImplementationName::Known(ImplementationName::{{value.name}}), implementation: LenientImplementationName::Known(ImplementationName::{{value.name}}),
arch: Arch(target_lexicon::Architecture::{{value.arch}}), arch: Arch(target_lexicon::Architecture::{{value.arch}}),
os: Os(target_lexicon::OperatingSystem::{{value.os}}), os: Os(target_lexicon::OperatingSystem::{{value.os}}),

View file

@ -260,8 +260,17 @@ impl PythonDownloadRequest {
return false; 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 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; return false;
} }
if version.is_freethreaded() { if version.is_freethreaded() {
@ -269,10 +278,6 @@ impl PythonDownloadRequest {
return false; 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 true
} }

View file

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

View file

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

View file

@ -17,6 +17,7 @@ Usage:
import argparse import argparse
import json import json
import logging import logging
import re
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@ -29,6 +30,7 @@ WORKSPACE_ROOT = CRATE_ROOT.parent.parent
VERSION_METADATA = CRATE_ROOT / "download-metadata.json" VERSION_METADATA = CRATE_ROOT / "download-metadata.json"
TEMPLATE = CRATE_ROOT / "src" / "downloads.inc.mustache" TEMPLATE = CRATE_ROOT / "src" / "downloads.inc.mustache"
TARGET = TEMPLATE.with_suffix("") TARGET = TEMPLATE.with_suffix("")
PRERELEASE_PATTERN = re.compile(r"(a|b|rc)(\d+)")
def prepare_name(name: str) -> str: def prepare_name(name: str) -> str:
@ -61,11 +63,21 @@ def prepare_arch(arch: str) -> str:
return arch.capitalize() 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: def prepare_value(value: dict) -> dict:
value["os"] = value["os"].title() value["os"] = value["os"].title()
value["arch"] = prepare_arch(value["arch"]) value["arch"] = prepare_arch(value["arch"])
value["name"] = prepare_name(value["name"]) value["name"] = prepare_name(value["name"])
value["libc"] = prepare_libc(value["libc"]) value["libc"] = prepare_libc(value["libc"])
value["prerelease"] = prepare_prerelease(value["prerelease"])
return value return value