diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 601694359783..91ec9011ed5c 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -64,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 = BTreeSet::new(); for (request, download_request) in requests.iter().zip(download_requests) { if matches!(requests.as_slice(), [PythonRequest::Default]) { writeln!(printer.stderr(), "Searching for Python installations")?; @@ -91,7 +91,7 @@ pub(crate) async fn install( } if reinstall { fs::remove_dir_all(installation.path())?; - uninstalled.push(installation.key().clone()); + uninstalled.insert(installation.key()); unfilled_requests.push(download_request); } } else { @@ -151,7 +151,7 @@ pub(crate) async fn install( }); } - let mut installed = vec![]; + let mut installed = BTreeSet::new(); let mut errors = vec![]; while let Some((key, result)) = tasks.next().await { match result { @@ -162,7 +162,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())?; @@ -176,7 +176,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(), @@ -190,29 +191,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 { @@ -222,6 +232,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!(), } } }