diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 476cd6ad7..dacf30b18 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -124,6 +124,24 @@ pub enum InstalledVersion<'a> { Url(&'a Url, &'a Version), } +impl<'a> InstalledVersion<'a> { + /// If it is a URL, return its value. + pub fn url(&self) -> Option<&Url> { + match self { + InstalledVersion::Version(_) => None, + InstalledVersion::Url(url, _) => Some(url), + } + } + + /// If it is a version, return its value. + pub fn version(&self) -> &Version { + match self { + InstalledVersion::Version(version) => version, + InstalledVersion::Url(_, version) => version, + } + } +} + impl std::fmt::Display for InstalledVersion<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/uv/src/commands/pip/loggers.rs b/crates/uv/src/commands/pip/loggers.rs index 9c48a5e48..d1a5e3fc3 100644 --- a/crates/uv/src/commands/pip/loggers.rs +++ b/crates/uv/src/commands/pip/loggers.rs @@ -1,13 +1,15 @@ +use std::collections::BTreeSet; use std::fmt; use std::fmt::Write; -use itertools::Itertools; -use owo_colors::OwoColorize; - -use distribution_types::{CachedDist, InstalledDist, InstalledMetadata, LocalDist, Name}; - use crate::commands::{elapsed, ChangeEvent, ChangeEventKind}; use crate::printer::Printer; +use distribution_types::{CachedDist, InstalledDist, InstalledMetadata, LocalDist, Name}; +use itertools::Itertools; +use owo_colors::OwoColorize; +use pep440_rs::Version; +use rustc_hash::{FxBuildHasher, FxHashMap}; +use uv_normalize::PackageName; /// A trait to handle logging during install operations. pub(crate) trait InstallLogger { @@ -223,6 +225,172 @@ impl InstallLogger for SummaryInstallLogger { } } +/// A logger that shows special output for the modification of the given target. +#[derive(Debug, Clone)] +pub(crate) struct UpgradeInstallLogger { + target: PackageName, +} + +impl UpgradeInstallLogger { + /// Create a new logger for the given target. + pub(crate) fn new(target: PackageName) -> Self { + Self { target } + } +} + +impl InstallLogger for UpgradeInstallLogger { + fn on_audit( + &self, + _count: usize, + _start: std::time::Instant, + _printer: Printer, + ) -> fmt::Result { + Ok(()) + } + + fn on_prepare( + &self, + _count: usize, + _start: std::time::Instant, + _printer: Printer, + ) -> fmt::Result { + Ok(()) + } + + fn on_uninstall( + &self, + _count: usize, + _start: std::time::Instant, + _printer: Printer, + ) -> fmt::Result { + Ok(()) + } + + fn on_install( + &self, + _count: usize, + _start: std::time::Instant, + _printer: Printer, + ) -> fmt::Result { + Ok(()) + } + + fn on_complete( + &self, + installed: Vec, + reinstalled: Vec, + uninstalled: Vec, + printer: Printer, + ) -> fmt::Result { + // Index the removals by package name. + let removals: FxHashMap<&PackageName, BTreeSet> = + reinstalled.iter().chain(uninstalled.iter()).fold( + FxHashMap::with_capacity_and_hasher( + reinstalled.len() + uninstalled.len(), + FxBuildHasher, + ), + |mut acc, distribution| { + acc.entry(distribution.name()) + .or_default() + .insert(distribution.installed_version().version().clone()); + acc + }, + ); + + // Index the additions by package name. + let additions: FxHashMap<&PackageName, BTreeSet> = installed.iter().fold( + FxHashMap::with_capacity_and_hasher(installed.len(), FxBuildHasher), + |mut acc, distribution| { + acc.entry(distribution.name()) + .or_default() + .insert(distribution.installed_version().version().clone()); + acc + }, + ); + + // Summarize the change for the target. + match (removals.get(&self.target), additions.get(&self.target)) { + (Some(removals), Some(additions)) => { + if removals == additions { + let reinstalls = additions + .iter() + .map(|version| format!("v{version}")) + .collect::>() + .join(", "); + writeln!( + printer.stderr(), + "{} {} {}", + "Reinstalled".yellow().bold(), + &self.target, + reinstalls + )?; + } else { + let removals = removals + .iter() + .map(|version| format!("v{version}")) + .collect::>() + .join(", "); + let additions = additions + .iter() + .map(|version| format!("v{version}")) + .collect::>() + .join(", "); + writeln!( + printer.stderr(), + "{} {} {} -> {}", + "Updated".green().bold(), + &self.target, + removals, + additions + )?; + } + } + (Some(removals), None) => { + let removals = removals + .iter() + .map(|version| format!("v{version}")) + .collect::>() + .join(", "); + writeln!( + printer.stderr(), + "{} {} {}", + "Removed".red().bold(), + &self.target, + removals + )?; + } + (None, Some(additions)) => { + let additions = additions + .iter() + .map(|version| format!("v{version}")) + .collect::>() + .join(", "); + writeln!( + printer.stderr(), + "{} {} {}", + "Added".green().bold(), + &self.target, + additions + )?; + } + (None, None) => { + writeln!( + printer.stderr(), + "{} {} {}", + "Modified".dimmed(), + &self.target.dimmed().bold(), + "environment".dimmed() + )?; + } + } + + // Follow-up with a detailed summary of all changes. + DefaultInstallLogger.on_complete(installed, reinstalled, uninstalled, printer)?; + + Ok(()) + } +} + /// A trait to handle logging during resolve operations. pub(crate) trait ResolveLogger { /// Log the completion of the operation. diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 5acaf4434..d6b53c8e7 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -61,13 +61,6 @@ pub(crate) fn remove_entrypoints(tool: &Tool) { } } -/// Represents the action to be performed on executables: update or install. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) enum InstallAction { - Update, - Install, -} - /// Installs tool executables for a given package and handles any conflicts. pub(crate) fn install_executables( environment: &PythonEnvironment, @@ -77,7 +70,6 @@ pub(crate) fn install_executables( force: bool, python: Option, requirements: Vec, - action: InstallAction, printer: Printer, ) -> anyhow::Result { let site_packages = SitePackages::from_environment(environment)?; @@ -102,8 +94,7 @@ pub(crate) fn install_executables( installed_dist.version(), )?; - // Determine the entry points targets - // Use a sorted collection for deterministic output + // Determine the entry points targets. Use a sorted collection for deterministic output. let target_entry_points = entry_points .into_iter() .map(|(name, source_path)| { @@ -180,13 +171,9 @@ pub(crate) fn install_executables( } else { "s" }; - let install_message = match action { - InstallAction::Install => "Installed", - InstallAction::Update => "Updated", - }; writeln!( printer.stderr(), - "{install_message} {} executable{s}: {}", + "Installed {} executable{s}: {}", target_entry_points.len(), target_entry_points .iter() @@ -194,7 +181,7 @@ pub(crate) fn install_executables( .join(", ") )?; - debug!("Adding receipt for tool `{}`", name); + debug!("Adding receipt for tool `{name}`"); let tool = Tool::new( requirements.into_iter().collect(), python, diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 32486fe7a..362292208 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -20,11 +20,10 @@ use uv_warnings::{warn_user, warn_user_once}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; -use crate::commands::tool::common::remove_entrypoints; -use crate::commands::{ - project::{resolve_environment, resolve_names, sync_environment, update_environment}, - tool::common::InstallAction, +use crate::commands::project::{ + resolve_environment, resolve_names, sync_environment, update_environment, }; +use crate::commands::tool::common::remove_entrypoints; use crate::commands::{reporters::PythonDownloadReporter, tool::common::install_executables}; use crate::commands::{ExitStatus, SharedState}; use crate::printer::Printer; @@ -352,7 +351,6 @@ pub(crate) async fn install( force || invalid_tool_receipt, python, requirements, - InstallAction::Install, printer, ) } diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 8e3b9da1d..e3dcedf68 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -4,12 +4,6 @@ use anyhow::Result; use owo_colors::OwoColorize; use tracing::debug; -use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; -use crate::commands::project::update_environment; -use crate::commands::tool::common::{remove_entrypoints, InstallAction}; -use crate::commands::{tool::common::install_executables, ExitStatus, SharedState}; -use crate::printer::Printer; -use crate::settings::ResolverInstallerSettings; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{Concurrency, PreviewMode}; @@ -19,6 +13,13 @@ use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use uv_warnings::warn_user_once; +use crate::commands::pip::loggers::{SummaryResolveLogger, UpgradeInstallLogger}; +use crate::commands::project::update_environment; +use crate::commands::tool::common::remove_entrypoints; +use crate::commands::{tool::common::install_executables, ExitStatus, SharedState}; +use crate::printer::Printer; +use crate::settings::ResolverInstallerSettings; + /// Upgrade a tool. pub(crate) async fn upgrade( name: Option, @@ -127,8 +128,8 @@ pub(crate) async fn upgrade( spec, &settings, &state, - Box::new(DefaultResolveLogger), - Box::new(DefaultInstallLogger), + Box::new(SummaryResolveLogger), + Box::new(UpgradeInstallLogger::new(name.clone())), preview, connectivity, concurrency, @@ -150,7 +151,6 @@ pub(crate) async fn upgrade( true, existing_tool_receipt.python().to_owned(), requirements.to_vec(), - InstallAction::Update, printer, )?; } diff --git a/crates/uv/tests/tool_upgrade.rs b/crates/uv/tests/tool_upgrade.rs index 6ffcb3644..8acfa07b1 100644 --- a/crates/uv/tests/tool_upgrade.rs +++ b/crates/uv/tests/tool_upgrade.rs @@ -50,14 +50,11 @@ fn test_tool_upgrade_name() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Updated babel v2.6.0 -> v2.14.0 - babel==2.6.0 + babel==2.14.0 - pytz==2018.5 - Updated 1 executable: pybabel + Installed 1 executable: pybabel "###); } @@ -126,21 +123,15 @@ fn test_tool_upgrade_all() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Updated babel v2.6.0 -> v2.14.0 - babel==2.6.0 + babel==2.14.0 - pytz==2018.5 - Updated 1 executable: pybabel - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Installed 1 executable: pybabel + Updated python-dotenv v0.10.2.post2 -> v1.0.1 - python-dotenv==0.10.2.post2 + python-dotenv==1.0.1 - Updated 1 executable: dotenv + Installed 1 executable: dotenv "###); } @@ -228,9 +219,7 @@ fn test_tool_upgrade_settings() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Audited [N] packages in [TIME] - Updated 2 executables: black, blackd + Installed 2 executables: black, blackd "###); // Upgrade `black`, but override the resolution. @@ -246,13 +235,10 @@ fn test_tool_upgrade_settings() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Updated black v23.1.0 -> v24.3.0 - black==23.1.0 + black==24.3.0 - Updated 2 executables: black, blackd + Installed 2 executables: black, blackd "###); } @@ -300,15 +286,12 @@ fn test_tool_upgrade_respect_constraints() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Updated babel v2.6.0 -> v2.9.1 - babel==2.6.0 + babel==2.9.1 - pytz==2018.5 + pytz==2024.1 - Updated 1 executable: pybabel + Installed 1 executable: pybabel "###); } @@ -358,15 +341,12 @@ fn test_tool_upgrade_constraint() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Updated babel v2.6.0 -> v2.13.1 - babel==2.6.0 + babel==2.13.1 - pytz==2018.5 + setuptools==69.2.0 - Updated 1 executable: pybabel + Installed 1 executable: pybabel "###); // Upgrade `babel` without a constraint. @@ -383,14 +363,11 @@ fn test_tool_upgrade_constraint() { ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] - Installed [N] packages in [TIME] + Updated babel v2.13.1 -> v2.14.0 - babel==2.13.1 + babel==2.14.0 - setuptools==69.2.0 - Updated 1 executable: pybabel + Installed 1 executable: pybabel "###); // Passing `--upgrade` explicitly should warn. @@ -409,8 +386,6 @@ fn test_tool_upgrade_constraint() { ----- stderr ----- warning: `--upgrade` is enabled by default on `uv tool upgrade` warning: `uv tool upgrade` is experimental and may change without warning - Resolved [N] packages in [TIME] - Audited [N] packages in [TIME] - Updated 1 executable: pybabel + Installed 1 executable: pybabel "###); }