Skip to content

Commit

Permalink
Update uv python install --reinstall to reinstall all previous vers…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
zanieb committed Jan 30, 2025
1 parent 48976e1 commit 8aa69b4
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 23 deletions.
18 changes: 18 additions & 0 deletions crates/uv-python/src/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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();
Expand Down
91 changes: 69 additions & 22 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::fmt::Write;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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() {
Expand Down
78 changes: 77 additions & 1 deletion crates/uv/tests/it/python_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand Down Expand Up @@ -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(&[])
Expand Down

0 comments on commit 8aa69b4

Please sign in to comment.