Refactor the Changelog for use in `report_dry_run` (#17039)

## Summary

Remove duplication in `report_dry_run` by making `Changelog` support
both local and remote dists. This is in support of #16653 and will form
a new basis for #16981.

This also involved refactoring `InstallLogger` and its implementations
to support dry run logging.

Additionally includes some minor refactoring in `SummaryInstallLogger`
and a fix to `InstalledVersion`.

See https://github.com/astral-sh/uv/compare/tk/dry-run-refactor for an
alternative approach (although obviously comes with some caveats).

## Test Plan

There are already quite a few tests which cover the output and they
pass. Manual testing was used to ensure styling stayed consistent.
This commit is contained in:
Tomasz Kramkowski 2025-12-12 10:37:30 +00:00 committed by GitHub
parent 38ae414682
commit 6ad80c5150
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 275 additions and 196 deletions

View File

@ -159,9 +159,9 @@ pub enum InstalledVersion<'a> {
Url(&'a DisplaySafeUrl, &'a Version), Url(&'a DisplaySafeUrl, &'a Version),
} }
impl InstalledVersion<'_> { impl<'a> InstalledVersion<'a> {
/// If it is a URL, return its value. /// If it is a URL, return its value.
pub fn url(&self) -> Option<&DisplaySafeUrl> { pub fn url(&self) -> Option<&'a DisplaySafeUrl> {
match self { match self {
Self::Version(_) => None, Self::Version(_) => None,
Self::Url(url, _) => Some(url), Self::Url(url, _) => Some(url),
@ -169,7 +169,7 @@ impl InstalledVersion<'_> {
} }
/// If it is a version, return its value. /// If it is a version, return its value.
pub fn version(&self) -> &Version { pub fn version(&self) -> &'a Version {
match self { match self {
Self::Version(version) => version, Self::Version(version) => version,
Self::Url(_, version) => version, Self::Url(_, version) => version,

View File

@ -2,7 +2,7 @@ use std::borrow::Cow;
use std::io::stdout; use std::io::stdout;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use std::{fmt::Display, fmt::Write, process::ExitCode}; use std::{fmt::Write, process::ExitCode};
use anstream::AutoStream; use anstream::AutoStream;
use anyhow::Context; use anyhow::Context;
@ -62,10 +62,8 @@ pub(crate) use tool::upgrade::upgrade as tool_upgrade;
use uv_cache::Cache; use uv_cache::Cache;
use uv_configuration::Concurrency; use uv_configuration::Concurrency;
pub(crate) use uv_console::human_readable_bytes; pub(crate) use uv_console::human_readable_bytes;
use uv_distribution_types::InstalledMetadata;
use uv_fs::{CWD, Simplified}; use uv_fs::{CWD, Simplified};
use uv_installer::compile_tree; use uv_installer::compile_tree;
use uv_normalize::PackageName;
use uv_python::PythonEnvironment; use uv_python::PythonEnvironment;
use uv_scripts::Pep723Script; use uv_scripts::Pep723Script;
pub(crate) use venv::venv; pub(crate) use venv::venv;
@ -73,6 +71,7 @@ pub(crate) use workspace::dir::dir;
pub(crate) use workspace::list::list; pub(crate) use workspace::list::list;
pub(crate) use workspace::metadata::metadata; pub(crate) use workspace::metadata::metadata;
use crate::commands::pip::operations::ChangedDist;
use crate::printer::Printer; use crate::printer::Printer;
mod auth; mod auth;
@ -148,15 +147,8 @@ pub(super) enum ChangeEventKind {
} }
#[derive(Debug)] #[derive(Debug)]
pub(super) struct ChangeEvent<'a, T: InstalledMetadata> { pub(super) struct ChangeEvent<'a> {
dist: &'a T, dist: &'a ChangedDist,
kind: ChangeEventKind,
}
#[derive(Debug)]
pub(super) struct DryRunEvent<T: Display> {
name: PackageName,
version: T,
kind: ChangeEventKind, kind: ChangeEventKind,
} }

View File

@ -1,5 +1,4 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fmt::Write;
use anyhow::Context; use anyhow::Context;
use itertools::Itertools; use itertools::Itertools;
@ -349,10 +348,7 @@ pub(crate) async fn pip_install(
debug!("Requirement satisfied: {requirement}"); debug!("Requirement satisfied: {requirement}");
} }
} }
DefaultInstallLogger.on_audit(requirements.len(), start, printer)?; DefaultInstallLogger.on_audit(requirements.len(), start, printer, dry_run)?;
if dry_run.enabled() {
writeln!(printer.stderr(), "Would make no changes")?;
}
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }

View File

@ -6,18 +6,24 @@ use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap}; use rustc_hash::{FxBuildHasher, FxHashMap};
use uv_distribution_types::{InstalledMetadata, Name}; use uv_configuration::DryRun;
use uv_distribution_types::Name;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version;
use crate::commands::pip::operations::Changelog; use crate::commands::pip::operations::{Changelog, ShortSpecifier};
use crate::commands::{ChangeEvent, ChangeEventKind, elapsed}; use crate::commands::{ChangeEvent, ChangeEventKind, elapsed};
use crate::printer::Printer; use crate::printer::Printer;
/// A trait to handle logging during install operations. /// A trait to handle logging during install operations.
pub(crate) trait InstallLogger { pub(crate) trait InstallLogger {
/// Log the completion of the audit phase. /// Log the completion of the audit phase.
fn on_audit(&self, count: usize, start: std::time::Instant, printer: Printer) -> fmt::Result; fn on_audit(
&self,
count: usize,
start: std::time::Instant,
printer: Printer,
dry_run: DryRun,
) -> fmt::Result;
/// Log the completion of the preparation phase. /// Log the completion of the preparation phase.
fn on_prepare( fn on_prepare(
@ -26,6 +32,7 @@ pub(crate) trait InstallLogger {
suffix: Option<&str>, suffix: Option<&str>,
start: std::time::Instant, start: std::time::Instant,
printer: Printer, printer: Printer,
dry_run: DryRun,
) -> fmt::Result; ) -> fmt::Result;
/// Log the completion of the uninstallation phase. /// Log the completion of the uninstallation phase.
@ -34,13 +41,20 @@ pub(crate) trait InstallLogger {
count: usize, count: usize,
start: std::time::Instant, start: std::time::Instant,
printer: Printer, printer: Printer,
dry_run: DryRun,
) -> fmt::Result; ) -> fmt::Result;
/// Log the completion of the installation phase. /// Log the completion of the installation phase.
fn on_install(&self, count: usize, start: std::time::Instant, printer: Printer) -> fmt::Result; fn on_install(
&self,
count: usize,
start: std::time::Instant,
printer: Printer,
dry_run: DryRun,
) -> fmt::Result;
/// Log the completion of the operation. /// Log the completion of the operation.
fn on_complete(&self, changelog: &Changelog, printer: Printer) -> fmt::Result; fn on_complete(&self, changelog: &Changelog, printer: Printer, dry_run: DryRun) -> fmt::Result;
} }
/// The default logger for install operations. /// The default logger for install operations.
@ -48,13 +62,19 @@ pub(crate) trait InstallLogger {
pub(crate) struct DefaultInstallLogger; pub(crate) struct DefaultInstallLogger;
impl InstallLogger for DefaultInstallLogger { impl InstallLogger for DefaultInstallLogger {
fn on_audit(&self, count: usize, start: std::time::Instant, printer: Printer) -> fmt::Result { fn on_audit(
&self,
count: usize,
start: std::time::Instant,
printer: Printer,
dry_run: DryRun,
) -> fmt::Result {
if count == 0 { if count == 0 {
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{}", "{}",
format!("Audited in {}", elapsed(start.elapsed())).dimmed() format!("Audited in {}", elapsed(start.elapsed())).dimmed()
) )?;
} else { } else {
let s = if count == 1 { "" } else { "s" }; let s = if count == 1 { "" } else { "s" };
writeln!( writeln!(
@ -66,8 +86,12 @@ impl InstallLogger for DefaultInstallLogger {
format!("in {}", elapsed(start.elapsed())).dimmed() format!("in {}", elapsed(start.elapsed())).dimmed()
) )
.dimmed() .dimmed()
) )?;
} }
if dry_run.enabled() {
writeln!(printer.stderr(), "Would make no changes")?;
}
Ok(())
} }
fn on_prepare( fn on_prepare(
@ -76,21 +100,26 @@ impl InstallLogger for DefaultInstallLogger {
suffix: Option<&str>, suffix: Option<&str>,
start: std::time::Instant, start: std::time::Instant,
printer: Printer, printer: Printer,
dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
let s = if count == 1 { "" } else { "s" }; let s = if count == 1 { "" } else { "s" };
let what = if let Some(suffix) = suffix {
format!("{count} package{s} {suffix}")
} else {
format!("{count} package{s}")
};
let what = what.bold();
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{}", "{}",
format!( if dry_run.enabled() {
"Prepared {} {}", format!("Would download {what}")
if let Some(suffix) = suffix { } else {
format!("{count} package{s} {suffix}") format!(
} else { "Prepared {what} {}",
format!("{count} package{s}") format!("in {}", elapsed(start.elapsed())).dimmed()
} )
.bold(), }
format!("in {}", elapsed(start.elapsed())).dimmed()
)
.dimmed() .dimmed()
) )
} }
@ -100,35 +129,57 @@ impl InstallLogger for DefaultInstallLogger {
count: usize, count: usize,
start: std::time::Instant, start: std::time::Instant,
printer: Printer, printer: Printer,
dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
let s = if count == 1 { "" } else { "s" }; let s = if count == 1 { "" } else { "s" };
let what = format!("{count} package{s}");
let what = what.bold();
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{}", "{}",
format!( if dry_run.enabled() {
"Uninstalled {} {}", format!("Would uninstall {what}")
format!("{count} package{s}").bold(), } else {
format!("in {}", elapsed(start.elapsed())).dimmed() format!(
) "Uninstalled {what} {}",
format!("in {}", elapsed(start.elapsed())).dimmed()
)
}
.dimmed() .dimmed()
) )
} }
fn on_install(&self, count: usize, start: std::time::Instant, printer: Printer) -> fmt::Result { fn on_install(
&self,
count: usize,
start: std::time::Instant,
printer: Printer,
dry_run: DryRun,
) -> fmt::Result {
let s = if count == 1 { "" } else { "s" }; let s = if count == 1 { "" } else { "s" };
let what = format!("{count} package{s}");
let what = what.bold();
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{}", "{}",
format!( if dry_run.enabled() {
"Installed {} {}", format!("Would install {what}")
format!("{count} package{s}").bold(), } else {
format!("in {}", elapsed(start.elapsed())).dimmed() format!(
) "Installed {what} {}",
format!("in {}", elapsed(start.elapsed())).dimmed()
)
}
.dimmed() .dimmed()
) )
} }
fn on_complete(&self, changelog: &Changelog, printer: Printer) -> fmt::Result { fn on_complete(
&self,
changelog: &Changelog,
printer: Printer,
_dry_run: DryRun,
) -> fmt::Result {
for event in changelog for event in changelog
.uninstalled .uninstalled
.iter() .iter()
@ -154,7 +205,7 @@ impl InstallLogger for DefaultInstallLogger {
.name() .name()
.cmp(b.dist.name()) .cmp(b.dist.name())
.then_with(|| a.kind.cmp(&b.kind)) .then_with(|| a.kind.cmp(&b.kind))
.then_with(|| a.dist.installed_version().cmp(&b.dist.installed_version())) .then_with(|| a.dist.long_specifier().cmp(&b.dist.long_specifier()))
}) })
{ {
match event.kind { match event.kind {
@ -164,7 +215,7 @@ impl InstallLogger for DefaultInstallLogger {
" {} {}{}", " {} {}{}",
"+".green(), "+".green(),
event.dist.name().bold(), event.dist.name().bold(),
event.dist.installed_version().dimmed() event.dist.long_specifier().dimmed()
)?; )?;
} }
ChangeEventKind::Removed => { ChangeEventKind::Removed => {
@ -173,7 +224,7 @@ impl InstallLogger for DefaultInstallLogger {
" {} {}{}", " {} {}{}",
"-".red(), "-".red(),
event.dist.name().bold(), event.dist.name().bold(),
event.dist.installed_version().dimmed() event.dist.long_specifier().dimmed()
)?; )?;
} }
ChangeEventKind::Reinstalled => { ChangeEventKind::Reinstalled => {
@ -182,7 +233,7 @@ impl InstallLogger for DefaultInstallLogger {
" {} {}{}", " {} {}{}",
"~".yellow(), "~".yellow(),
event.dist.name().bold(), event.dist.name().bold(),
event.dist.installed_version().dimmed() event.dist.long_specifier().dimmed()
)?; )?;
} }
} }
@ -202,6 +253,7 @@ impl InstallLogger for SummaryInstallLogger {
_count: usize, _count: usize,
_start: std::time::Instant, _start: std::time::Instant,
_printer: Printer, _printer: Printer,
_dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
Ok(()) Ok(())
} }
@ -212,6 +264,7 @@ impl InstallLogger for SummaryInstallLogger {
_suffix: Option<&str>, _suffix: Option<&str>,
_start: std::time::Instant, _start: std::time::Instant,
_printer: Printer, _printer: Printer,
_dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
Ok(()) Ok(())
} }
@ -221,35 +274,27 @@ impl InstallLogger for SummaryInstallLogger {
count: usize, count: usize,
start: std::time::Instant, start: std::time::Instant,
printer: Printer, printer: Printer,
dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
let s = if count == 1 { "" } else { "s" }; DefaultInstallLogger.on_uninstall(count, start, printer, dry_run)
writeln!(
printer.stderr(),
"{}",
format!(
"Uninstalled {} {}",
format!("{count} package{s}").bold(),
format!("in {}", elapsed(start.elapsed())).dimmed()
)
.dimmed()
)
} }
fn on_install(&self, count: usize, start: std::time::Instant, printer: Printer) -> fmt::Result { fn on_install(
let s = if count == 1 { "" } else { "s" }; &self,
writeln!( count: usize,
printer.stderr(), start: std::time::Instant,
"{}", printer: Printer,
format!( dry_run: DryRun,
"Installed {} {}", ) -> fmt::Result {
format!("{count} package{s}").bold(), DefaultInstallLogger.on_install(count, start, printer, dry_run)
format!("in {}", elapsed(start.elapsed())).dimmed()
)
.dimmed()
)
} }
fn on_complete(&self, _changelog: &Changelog, _printer: Printer) -> fmt::Result { fn on_complete(
&self,
_changelog: &Changelog,
_printer: Printer,
_dry_run: DryRun,
) -> fmt::Result {
Ok(()) Ok(())
} }
} }
@ -273,6 +318,7 @@ impl InstallLogger for UpgradeInstallLogger {
_count: usize, _count: usize,
_start: std::time::Instant, _start: std::time::Instant,
_printer: Printer, _printer: Printer,
_dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
Ok(()) Ok(())
} }
@ -283,6 +329,7 @@ impl InstallLogger for UpgradeInstallLogger {
_suffix: Option<&str>, _suffix: Option<&str>,
_start: std::time::Instant, _start: std::time::Instant,
_printer: Printer, _printer: Printer,
_dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
Ok(()) Ok(())
} }
@ -292,6 +339,7 @@ impl InstallLogger for UpgradeInstallLogger {
_count: usize, _count: usize,
_start: std::time::Instant, _start: std::time::Instant,
_printer: Printer, _printer: Printer,
_dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
Ok(()) Ok(())
} }
@ -301,31 +349,38 @@ impl InstallLogger for UpgradeInstallLogger {
_count: usize, _count: usize,
_start: std::time::Instant, _start: std::time::Instant,
_printer: Printer, _printer: Printer,
_dry_run: DryRun,
) -> fmt::Result { ) -> fmt::Result {
Ok(()) Ok(())
} }
fn on_complete(&self, changelog: &Changelog, printer: Printer) -> fmt::Result { fn on_complete(
&self,
changelog: &Changelog,
printer: Printer,
// TODO(tk): Adjust format for dry_run
_dry_run: DryRun,
) -> fmt::Result {
// Index the removals by package name. // Index the removals by package name.
let removals: FxHashMap<&PackageName, BTreeSet<Version>> = let removals: FxHashMap<&PackageName, BTreeSet<ShortSpecifier>> =
changelog.uninstalled.iter().fold( changelog.uninstalled.iter().fold(
FxHashMap::with_capacity_and_hasher(changelog.uninstalled.len(), FxBuildHasher), FxHashMap::with_capacity_and_hasher(changelog.uninstalled.len(), FxBuildHasher),
|mut acc, distribution| { |mut acc, distribution| {
acc.entry(distribution.name()) acc.entry(distribution.name())
.or_default() .or_default()
.insert(distribution.installed_version().version().clone()); .insert(distribution.short_specifier());
acc acc
}, },
); );
// Index the additions by package name. // Index the additions by package name.
let additions: FxHashMap<&PackageName, BTreeSet<Version>> = let additions: FxHashMap<&PackageName, BTreeSet<ShortSpecifier>> =
changelog.installed.iter().fold( changelog.installed.iter().fold(
FxHashMap::with_capacity_and_hasher(changelog.installed.len(), FxBuildHasher), FxHashMap::with_capacity_and_hasher(changelog.installed.len(), FxBuildHasher),
|mut acc, distribution| { |mut acc, distribution| {
acc.entry(distribution.name()) acc.entry(distribution.name())
.or_default() .or_default()
.insert(distribution.installed_version().version().clone()); .insert(distribution.short_specifier());
acc acc
}, },
); );
@ -407,7 +462,7 @@ impl InstallLogger for UpgradeInstallLogger {
} }
// Follow-up with a detailed summary of all changes. // Follow-up with a detailed summary of all changes.
DefaultInstallLogger.on_complete(changelog, printer)?; DefaultInstallLogger.on_complete(changelog, printer, _dry_run)?;
Ok(()) Ok(())
} }

View File

@ -19,15 +19,17 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_distribution::{DistributionDatabase, SourcedDependencyGroups}; use uv_distribution::{DistributionDatabase, SourcedDependencyGroups};
use uv_distribution_types::{ use uv_distribution_types::{
CachedDist, Diagnostic, InstalledDist, LocalDist, NameRequirementSpecification, Requirement, CachedDist, Diagnostic, Dist, InstalledDist, InstalledVersion, LocalDist,
ResolutionDiagnostic, UnresolvedRequirement, UnresolvedRequirementSpecification, NameRequirementSpecification, Requirement, ResolutionDiagnostic, UnresolvedRequirement,
UnresolvedRequirementSpecification, VersionOrUrlRef,
}; };
use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution}; use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_install_wheel::LinkMode; use uv_install_wheel::LinkMode;
use uv_installer::{InstallationStrategy, Plan, Planner, Preparer, SitePackages}; use uv_installer::{InstallationStrategy, Plan, Planner, Preparer, SitePackages};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep508::{MarkerEnvironment, RequirementOrigin}; use uv_pep440::Version;
use uv_pep508::{MarkerEnvironment, RequirementOrigin, VerbatimUrl};
use uv_platform_tags::Tags; use uv_platform_tags::Tags;
use uv_preview::Preview; use uv_preview::Preview;
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment}; use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
@ -44,9 +46,9 @@ use uv_tool::InstalledTools;
use uv_types::{BuildContext, HashStrategy, InFlight, InstalledPackagesProvider}; use uv_types::{BuildContext, HashStrategy, InFlight, InstalledPackagesProvider};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger, ResolveLogger}; use crate::commands::compile_bytecode;
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::reporters::{InstallReporter, PrepareReporter, ResolverReporter}; use crate::commands::reporters::{InstallReporter, PrepareReporter, ResolverReporter};
use crate::commands::{ChangeEventKind, DryRunEvent, compile_bytecode};
use crate::printer::Printer; use crate::printer::Printer;
/// Consolidate the requirements for an installation. /// Consolidate the requirements for an installation.
@ -381,30 +383,104 @@ pub(crate) enum Modifications {
Exact, Exact,
} }
/// A distribution which was or would be modified
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ChangedDist {
Local(LocalDist),
Remote(Arc<Dist>),
}
impl Name for ChangedDist {
fn name(&self) -> &PackageName {
match self {
Self::Local(dist) => dist.name(),
Self::Remote(dist) => dist.name(),
}
}
}
/// The [`Version`] or [`VerbatimUrl`] for a changed dist.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub(crate) enum ShortSpecifier<'a> {
Version(&'a Version),
Url(&'a VerbatimUrl),
}
impl std::fmt::Display for ShortSpecifier<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Version(version) => version.fmt(f),
Self::Url(url) => write!(f, " @ {url}"),
}
}
}
/// The [`InstalledVersion`] or [`VerbatimUrl`] for a changed dist.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub(crate) enum LongSpecifier<'a> {
InstalledVersion(InstalledVersion<'a>),
Url(&'a VerbatimUrl),
}
impl std::fmt::Display for LongSpecifier<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InstalledVersion(version) => version.fmt(f),
Self::Url(url) => write!(f, " @ {url}"),
}
}
}
impl ChangedDist {
pub(crate) fn short_specifier(&self) -> ShortSpecifier<'_> {
match self {
Self::Local(dist) => ShortSpecifier::Version(dist.installed_version().version()),
Self::Remote(dist) => match dist.version_or_url() {
VersionOrUrlRef::Version(version) => ShortSpecifier::Version(version),
VersionOrUrlRef::Url(url) => ShortSpecifier::Url(url),
},
}
}
pub(crate) fn long_specifier(&self) -> LongSpecifier<'_> {
match self {
Self::Local(dist) => LongSpecifier::InstalledVersion(dist.installed_version()),
Self::Remote(dist) => match dist.version_or_url() {
VersionOrUrlRef::Version(version) => {
LongSpecifier::InstalledVersion(InstalledVersion::Version(version))
}
VersionOrUrlRef::Url(url) => LongSpecifier::Url(url),
},
}
}
}
/// A summary of the changes made to the environment during an installation. /// A summary of the changes made to the environment during an installation.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub(crate) struct Changelog { pub(crate) struct Changelog {
/// The distributions that were installed. /// The distributions that were installed.
pub(crate) installed: HashSet<LocalDist>, pub(crate) installed: HashSet<ChangedDist>,
/// The distributions that were uninstalled. /// The distributions that were uninstalled.
pub(crate) uninstalled: HashSet<LocalDist>, pub(crate) uninstalled: HashSet<ChangedDist>,
/// The distributions that were reinstalled. /// The distributions that were reinstalled.
pub(crate) reinstalled: HashSet<LocalDist>, pub(crate) reinstalled: HashSet<ChangedDist>,
} }
impl Changelog { impl Changelog {
/// Create a [`Changelog`] from a list of installed and uninstalled distributions. /// Create a [`Changelog`] from two iterators of [`ChangedDist`]s.
pub(crate) fn new(installed: Vec<CachedDist>, uninstalled: Vec<InstalledDist>) -> Self { pub(crate) fn new<I, U>(installed: I, uninstalled: U) -> Self
where
I: IntoIterator<Item = ChangedDist>,
U: IntoIterator<Item = ChangedDist>,
{
// SAFETY: This is allowed because `LocalDist` implements `Hash` and `Eq` based solely on // SAFETY: This is allowed because `LocalDist` implements `Hash` and `Eq` based solely on
// the inner `kind`, and omits the types that rely on internal mutability. // the inner `kind`, and omits the types that rely on internal mutability.
#[allow(clippy::mutable_key_type)] #[allow(clippy::mutable_key_type)]
let mut uninstalled: HashSet<_> = uninstalled.into_iter().map(LocalDist::from).collect(); let mut uninstalled: HashSet<_> = uninstalled.into_iter().collect();
let (reinstalled, installed): (HashSet<_>, HashSet<_>) = installed let (reinstalled, installed): (HashSet<_>, HashSet<_>) = installed
.into_iter() .into_iter()
.map(LocalDist::from)
.partition(|dist| uninstalled.contains(dist)); .partition(|dist| uninstalled.contains(dist));
uninstalled.retain(|dist| !reinstalled.contains(dist)); uninstalled.retain(|dist| !reinstalled.contains(dist));
Self { Self {
@ -414,13 +490,21 @@ impl Changelog {
} }
} }
/// Create a [`Changelog`] from a list of local distributions.
pub(crate) fn from_local(installed: Vec<CachedDist>, uninstalled: Vec<InstalledDist>) -> Self {
Self::new(
installed
.into_iter()
.map(|dist| ChangedDist::Local(dist.into())),
uninstalled
.into_iter()
.map(|dist| ChangedDist::Local(dist.into())),
)
}
/// Create a [`Changelog`] from a list of installed distributions. /// Create a [`Changelog`] from a list of installed distributions.
pub(crate) fn from_installed(installed: Vec<CachedDist>) -> Self { pub(crate) fn from_installed(installed: Vec<CachedDist>) -> Self {
Self { Self::from_local(installed, Vec::new())
installed: installed.into_iter().map(LocalDist::from).collect(),
uninstalled: HashSet::default(),
reinstalled: HashSet::default(),
}
} }
/// Returns `true` if the changelog includes a distribution with the given name, either via /// Returns `true` if the changelog includes a distribution with the given name, either via
@ -485,8 +569,15 @@ pub(crate) async fn install(
.context("Failed to determine installation plan")?; .context("Failed to determine installation plan")?;
if dry_run.enabled() { if dry_run.enabled() {
report_dry_run(dry_run, resolution, plan, modifications, start, printer)?; return report_dry_run(
return Ok(Changelog::default()); dry_run,
resolution,
plan,
modifications,
start,
logger.as_ref(),
printer,
);
} }
let Plan { let Plan {
@ -509,7 +600,7 @@ pub(crate) async fn install(
&& extraneous.is_empty() && extraneous.is_empty()
&& !compile && !compile
{ {
logger.on_audit(resolution.len(), start, printer)?; logger.on_audit(resolution.len(), start, printer, dry_run)?;
return Ok(Changelog::default()); return Ok(Changelog::default());
} }
@ -590,10 +681,10 @@ pub(crate) async fn install(
} }
// Construct a summary of the changes made to the environment. // Construct a summary of the changes made to the environment.
let changelog = Changelog::new(installs, uninstalls); let changelog = Changelog::from_local(installs, uninstalls);
// Notify the user of any environment modifications. // Notify the user of any environment modifications.
logger.on_complete(&changelog, printer)?; logger.on_complete(&changelog, printer, dry_run)?;
Ok(changelog) Ok(changelog)
} }
@ -660,7 +751,13 @@ async fn execute_plan(
.prepare(remote.clone(), in_flight, resolution) .prepare(remote.clone(), in_flight, resolution)
.await?; .await?;
logger.on_prepare(wheels.len(), phase.map(InstallPhase::label), start, printer)?; logger.on_prepare(
wheels.len(),
phase.map(InstallPhase::label),
start,
printer,
DryRun::Disabled,
)?;
wheels wheels
}; };
@ -702,7 +799,7 @@ async fn execute_plan(
} }
} }
logger.on_uninstall(uninstalls.len(), start, printer)?; logger.on_uninstall(uninstalls.len(), start, printer, DryRun::Disabled)?;
} }
// Install the resolved distributions. // Install the resolved distributions.
@ -721,7 +818,7 @@ async fn execute_plan(
// task. // task.
.install_blocking(installs)?; .install_blocking(installs)?;
logger.on_install(installs.len(), start, printer)?; logger.on_install(installs.len(), start, printer, DryRun::Disabled)?;
} }
Ok((installs, uninstalls)) Ok((installs, uninstalls))
@ -840,8 +937,9 @@ fn report_dry_run(
plan: Plan, plan: Plan,
modifications: Modifications, modifications: Modifications,
start: std::time::Instant, start: std::time::Instant,
logger: &dyn InstallLogger,
printer: Printer, printer: Printer,
) -> Result<(), Error> { ) -> Result<Changelog, Error> {
let Plan { let Plan {
cached, cached,
remote, remote,
@ -857,25 +955,15 @@ fn report_dry_run(
// Nothing to do. // Nothing to do.
if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() && extraneous.is_empty() { if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() && extraneous.is_empty() {
DefaultInstallLogger.on_audit(resolution.len(), start, printer)?; logger.on_audit(resolution.len(), start, printer, dry_run)?;
writeln!(printer.stderr(), "Would make no changes")?; return Ok(Changelog::default());
return Ok(());
} }
// Download, build, and unzip any missing distributions. // Download, build, and unzip any missing distributions.
let wheels = if remote.is_empty() { let wheels = if remote.is_empty() {
vec![] vec![]
} else { } else {
let s = if remote.len() == 1 { "" } else { "s" }; logger.on_prepare(remote.len(), None, start, printer, dry_run)?;
writeln!(
printer.stderr(),
"{}",
format!(
"Would download {}",
format!("{} package{}", remote.len(), s).bold(),
)
.dimmed()
)?;
remote.clone() remote.clone()
}; };
@ -883,87 +971,35 @@ fn report_dry_run(
let uninstalls = extraneous.len() + reinstalls.len(); let uninstalls = extraneous.len() + reinstalls.len();
if uninstalls > 0 { if uninstalls > 0 {
let s = if uninstalls == 1 { "" } else { "s" }; logger.on_uninstall(uninstalls, start, printer, dry_run)?;
writeln!(
printer.stderr(),
"{}",
format!(
"Would uninstall {}",
format!("{uninstalls} package{s}").bold(),
)
.dimmed()
)?;
} }
// Install the resolved distributions. // Install the resolved distributions.
let installs = wheels.len() + cached.len(); let installs = wheels.len() + cached.len();
if installs > 0 { if installs > 0 {
let s = if installs == 1 { "" } else { "s" }; logger.on_install(installs, start, printer, dry_run)?;
writeln!(
printer.stderr(),
"{}",
format!("Would install {}", format!("{installs} package{s}").bold()).dimmed()
)?;
} }
// TODO(charlie): DRY this up with `report_modifications`. The types don't quite line up. let uninstalled = reinstalls
for event in reinstalls
.into_iter() .into_iter()
.chain(extraneous.into_iter()) .chain(extraneous)
.map(|distribution| DryRunEvent { .map(|dist| ChangedDist::Local(dist.into()));
name: distribution.name().clone(), let installed = wheels.into_iter().map(ChangedDist::Remote).chain(
version: distribution.installed_version().to_string(), cached
kind: ChangeEventKind::Removed, .into_iter()
}) .map(|dist| ChangedDist::Local(dist.into())),
.chain(wheels.into_iter().map(|distribution| DryRunEvent { );
name: distribution.name().clone(),
version: distribution.version_or_url().to_string(), let changelog = Changelog::new(installed, uninstalled);
kind: ChangeEventKind::Added,
})) logger.on_complete(&changelog, printer, dry_run)?;
.chain(cached.into_iter().map(|distribution| DryRunEvent {
name: distribution.name().clone(),
version: distribution.installed_version().to_string(),
kind: ChangeEventKind::Added,
}))
.sorted_unstable_by(|a, b| a.name.cmp(&b.name).then_with(|| a.kind.cmp(&b.kind)))
{
match event.kind {
ChangeEventKind::Added => {
writeln!(
printer.stderr(),
" {} {}{}",
"+".green(),
event.name.bold(),
event.version.dimmed()
)?;
}
ChangeEventKind::Removed => {
writeln!(
printer.stderr(),
" {} {}{}",
"-".red(),
event.name.bold(),
event.version.dimmed()
)?;
}
ChangeEventKind::Reinstalled => {
writeln!(
printer.stderr(),
" {} {}{}",
"~".yellow(),
event.name.bold(),
event.version.dimmed()
)?;
}
}
}
if matches!(dry_run, DryRun::Check) { if matches!(dry_run, DryRun::Check) {
return Err(Error::OutdatedEnvironment); return Err(Error::OutdatedEnvironment);
} }
Ok(()) Ok(changelog)
} }
/// Report any diagnostics on resolved distributions. /// Report any diagnostics on resolved distributions.

View File

@ -10,8 +10,8 @@ use thiserror::Error;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{ use uv_configuration::{
BuildOptions, Concurrency, Constraints, DependencyGroups, IndexStrategy, KeyringProviderType, BuildOptions, Concurrency, Constraints, DependencyGroups, DryRun, IndexStrategy,
NoBinary, NoBuild, SourceStrategy, KeyringProviderType, NoBinary, NoBuild, SourceStrategy,
}; };
use uv_dispatch::{BuildDispatch, SharedState}; use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution_types::{ use uv_distribution_types::{
@ -310,7 +310,7 @@ pub(crate) async fn venv(
.map_err(|err| VenvError::Seed(err.into()))?; .map_err(|err| VenvError::Seed(err.into()))?;
let changelog = Changelog::from_installed(installed); let changelog = Changelog::from_installed(installed);
DefaultInstallLogger.on_complete(&changelog, printer)?; DefaultInstallLogger.on_complete(&changelog, printer, DryRun::Disabled)?;
} }
// Determine the appropriate activation command. // Determine the appropriate activation command.