Update uv python install --reinstall to reinstall all previous versions (#11072)

Since we're shipping substantive updates to Python versions frequently,
I want to lower the bar for reinstalling with the latest distributions.

There's a follow-up task that's documented in a test case at
https://github.com/astral-sh/uv/pull/11072/files#diff-f499c776e1d8cc5e55d7620786e32e8732b675abd98e246c0971130f5de9ed50R157-R158
This commit is contained in:
Zanie Blue 2025-01-30 10:08:06 -06:00 committed by GitHub
parent d517b1ca26
commit 586bab32b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 164 additions and 23 deletions

View file

@ -28,6 +28,7 @@ use crate::implementation::{
};
use crate::installation::PythonInstallationKey;
use crate::libc::LibcDetectionError;
use crate::managed::ManagedPythonInstallation;
use crate::platform::{self, Arch, Libc, Os};
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
@ -344,6 +345,23 @@ impl PythonDownloadRequest {
}
}
impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
fn from(installation: &ManagedPythonInstallation) -> Self {
let key = installation.key();
Self::new(
Some(VersionRequest::from(&key.version())),
match &key.implementation {
LenientImplementationName::Known(implementation) => Some(*implementation),
LenientImplementationName::Unknown(name) => unreachable!("Managed Python installations are expected to always have known implementation names, found {name}"),
},
Some(key.arch),
Some(key.os),
Some(key.libc),
Some(key.prerelease.is_some()),
)
}
}
impl Display for PythonDownloadRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::fmt::Write;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
@ -164,7 +165,12 @@ pub(crate) async fn install(
.unwrap_or_else(|| {
// If no version file is found and no requests were made
is_default_install = true;
vec![PythonRequest::Default]
vec![if reinstall {
// On bare `--reinstall`, reinstall all Python versions
PythonRequest::Any
} else {
PythonRequest::Default
}]
})
.into_iter()
.map(InstallRequest::new)
@ -193,35 +199,76 @@ pub(crate) async fn install(
// Find requests that are already satisfied
let mut changelog = Changelog::default();
let (satisfied, unsatisfied): (Vec<_>, Vec<_>) = requests.iter().partition_map(|request| {
if let Some(installation) = existing_installations
.iter()
.find(|installation| request.matches_installation(installation))
{
changelog.existing.insert(installation.key().clone());
if reinstall {
debug!(
"Ignoring match `{}` for request `{}` due to `--reinstall` flag",
installation.key().green(),
request.cyan()
);
let (satisfied, unsatisfied): (Vec<_>, Vec<_>) = if reinstall {
// In the reinstall case, we want to iterate over all matching installations instead of
// stopping at the first match.
Either::Right(request)
} else {
let mut unsatisfied: Vec<Cow<InstallRequest>> =
Vec::with_capacity(existing_installations.len() + requests.len());
for request in &requests {
if existing_installations.is_empty() {
debug!("No installation found for request `{}`", request.cyan());
unsatisfied.push(Cow::Borrowed(request));
}
for installation in existing_installations
.iter()
.filter(|installation| request.matches_installation(installation))
{
changelog.existing.insert(installation.key().clone());
if matches!(&request.request, &PythonRequest::Any) {
// Construct a install request matching the existing installation
match InstallRequest::new(PythonRequest::Key(installation.into())) {
Ok(request) => {
debug!("Will reinstall `{}`", installation.key().green());
unsatisfied.push(Cow::Owned(request));
}
Err(err) => {
// This shouldn't really happen, but maybe a new version of uv dropped
// support for a key we previously supported
warn_user!(
"Failed to create reinstall request for existing installation `{}`: {err}",
installation.key().green()
);
}
}
} else {
// TODO(zanieb): This isn't really right! But we need `--upgrade` or similar
// to handle this case correctly without causing a breaking change.
// If we have real requests, just ignore the existing installation
debug!(
"Ignoring match `{}` for request `{}` due to `--reinstall` flag",
installation.key().green(),
request.cyan()
);
unsatisfied.push(Cow::Borrowed(request));
break;
}
}
}
(vec![], unsatisfied)
} else {
// If we can find one existing installation that matches the request, it is satisfied
requests.iter().partition_map(|request| {
if let Some(installation) = existing_installations
.iter()
.find(|installation| request.matches_installation(installation))
{
debug!(
"Found `{}` for request `{}`",
installation.key().green(),
request.cyan(),
);
Either::Left(installation)
} else {
debug!("No installation found for request `{}`", request.cyan());
Either::Right(Cow::Borrowed(request))
}
} else {
debug!("No installation found for request `{}`", request.cyan());
Either::Right(request)
}
});
})
};
// Check if Python downloads are banned
if matches!(python_downloads, PythonDownloads::Never) && !unsatisfied.is_empty() {

View file

@ -54,7 +54,7 @@ fn python_install() {
"###);
// You can opt-in to a reinstall
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
uv_snapshot!(context.filters(), context.python_install().arg("3.13").arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -91,6 +91,82 @@ fn python_install() {
"###);
}
#[test]
fn python_reinstall() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install a couple versions
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("3.13"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 2 versions in [TIME]
+ cpython-3.12.8-[PLATFORM]
+ cpython-3.13.1-[PLATFORM]
"###);
// Reinstall a single version
uv_snapshot!(context.filters(), context.python_install().arg("3.13").arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.1 in [TIME]
~ cpython-3.13.1-[PLATFORM]
"###);
// Reinstall multiple versions
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 2 versions in [TIME]
~ cpython-3.12.8-[PLATFORM]
~ cpython-3.13.1-[PLATFORM]
"###);
}
#[test]
fn python_reinstall_patch() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install a couple patch versions
uv_snapshot!(context.filters(), context.python_install().arg("3.12.6").arg("3.12.7"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 2 versions in [TIME]
+ cpython-3.12.6-[PLATFORM]
+ cpython-3.12.7-[PLATFORM]
"###);
// Reinstall all "3.12" versions
// TODO(zanieb): This doesn't work today, because we need this to install the "latest" as there
// is no workflow for `--upgrade` yet
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("--reinstall"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.12.8 in [TIME]
+ cpython-3.12.8-[PLATFORM]
"###);
}
#[test]
fn python_install_automatic() {
let context: TestContext = TestContext::new_with_versions(&[])