diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 549eb3808f86..13fd159bf678 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -133,7 +133,7 @@ pub enum EnvironmentPreference { Any, } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PythonVariant { #[default] Default, diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index c6bf4e81669e..b9afa8e486b6 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -405,5 +405,6 @@ impl Ord for PythonInstallationKey { .then_with(|| self.os.to_string().cmp(&other.os.to_string())) .then_with(|| self.arch.to_string().cmp(&other.arch.to_string())) .then_with(|| self.libc.to_string().cmp(&other.libc.to_string())) + .then_with(|| self.variant.cmp(&other.variant)) } } diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index bb7c4be0b54c..322477b412be 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -3,6 +3,7 @@ use futures::stream::FuturesUnordered; use futures::StreamExt; use itertools::Itertools; use owo_colors::OwoColorize; +use rustc_hash::FxHashSet; use std::collections::BTreeSet; use std::fmt::Write; use std::path::Path; @@ -63,7 +64,7 @@ pub(crate) async fn install( .inspect(|installation| debug!("Found existing installation {}", installation.key())) .collect(); let mut unfilled_requests = Vec::new(); - let mut uninstalled = Vec::new(); + let mut uninstalled = FxHashSet::default(); for (request, download_request) in requests.iter().zip(download_requests) { if matches!(requests.as_slice(), [PythonRequest::Default]) { writeln!(printer.stderr(), "Searching for Python installations")?; @@ -89,7 +90,7 @@ pub(crate) async fn install( )?; } if reinstall { - uninstalled.push(installation.key().clone()); + uninstalled.insert(installation.key()); unfilled_requests.push(download_request); } } else { @@ -155,7 +156,7 @@ pub(crate) async fn install( }); } - let mut installed = vec![]; + let mut installed = FxHashSet::default(); let mut errors = vec![]; while let Some((key, result)) = tasks.next().await { match result { @@ -166,7 +167,7 @@ pub(crate) async fn install( DownloadResult::Fetched(path) => path, }; - installed.push(key.clone()); + installed.insert(key); // Ensure the installations have externally managed markers let managed = ManagedPythonInstallation::new(path.clone())?; @@ -180,7 +181,8 @@ pub(crate) async fn install( } if !installed.is_empty() { - if let [installed] = installed.as_slice() { + if installed.len() == 1 { + let installed = installed.iter().next().unwrap(); // Ex) "Installed Python 3.9.7 in 1.68s" writeln!( printer.stderr(), @@ -194,29 +196,38 @@ pub(crate) async fn install( )?; } else { // Ex) "Installed 2 versions in 1.68s" - let s = if installed.len() == 1 { "" } else { "s" }; writeln!( printer.stderr(), "{}", format!( "Installed {} {}", - format!("{} version{s}", installed.len()).bold(), + format!("{} versions", installed.len()).bold(), format!("in {}", elapsed(start.elapsed())).dimmed() ) .dimmed() )?; } + let reinstalled = uninstalled + .intersection(&installed) + .copied() + .collect::>(); + let uninstalled = uninstalled.difference(&reinstalled).copied(); + let installed = installed.difference(&reinstalled).copied(); + for event in uninstalled - .into_iter() .map(|key| ChangeEvent { - key, + key: key.clone(), kind: ChangeEventKind::Removed, }) - .chain(installed.into_iter().map(|key| ChangeEvent { - key, + .chain(installed.map(|key| ChangeEvent { + key: key.clone(), kind: ChangeEventKind::Added, })) + .chain(reinstalled.iter().map(|&key| ChangeEvent { + key: key.clone(), + kind: ChangeEventKind::Reinstalled, + })) .sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind))) { match event.kind { @@ -226,6 +237,9 @@ pub(crate) async fn install( ChangeEventKind::Removed => { writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?; } + ChangeEventKind::Reinstalled => { + writeln!(printer.stderr(), " {} {}", "~".yellow(), event.key.bold(),)?; + } } } } diff --git a/crates/uv/src/commands/python/mod.rs b/crates/uv/src/commands/python/mod.rs index 80a39fae14e1..afc700d2335b 100644 --- a/crates/uv/src/commands/python/mod.rs +++ b/crates/uv/src/commands/python/mod.rs @@ -11,6 +11,8 @@ pub(super) enum ChangeEventKind { Removed, /// The Python version was installed. Added, + /// The Python version was reinstalled. + Reinstalled, } #[derive(Debug)] diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index 531cda41586e..cd83cecb4e42 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -179,12 +179,10 @@ async fn do_uninstall( .sorted_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.kind.cmp(&b.kind))) { match event.kind { - ChangeEventKind::Added => { - writeln!(printer.stderr(), " {} {}", "+".green(), event.key.bold())?; - } ChangeEventKind::Removed => { writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?; } + _ => unreachable!(), } } }