Refactor dry run reporting

Remove the reporter by rolling the functionality into the normal flow.

This approach has the side effect of eagerly preparing packages.
This commit is contained in:
Tomasz (Tom) Kramkowski 2025-12-08 17:08:01 +00:00
parent a0a0a8d0e5
commit d47ded75d2
7 changed files with 94 additions and 223 deletions

View File

@ -2,7 +2,7 @@ use std::borrow::Cow;
use std::io::stdout;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{fmt::Display, fmt::Write, process::ExitCode};
use std::{fmt::Write, process::ExitCode};
use anstream::AutoStream;
use anyhow::Context;
@ -65,7 +65,6 @@ pub(crate) use uv_console::human_readable_bytes;
use uv_distribution_types::InstalledMetadata;
use uv_fs::{CWD, Simplified};
use uv_installer::compile_tree;
use uv_normalize::PackageName;
use uv_python::PythonEnvironment;
use uv_scripts::Pep723Script;
pub(crate) use venv::venv;
@ -153,13 +152,6 @@ pub(super) struct ChangeEvent<'a, T: InstalledMetadata> {
kind: ChangeEventKind,
}
#[derive(Debug)]
pub(super) struct DryRunEvent<T: Display> {
name: PackageName,
version: T,
kind: ChangeEventKind,
}
/// Compile all Python source files in site-packages to bytecode, to speed up the
/// initial run of any subsequent executions.
///

View File

@ -33,7 +33,6 @@ pub(crate) trait InstallLogger {
suffix: Option<&str>,
start: std::time::Instant,
printer: Printer,
dry_run: DryRun,
) -> fmt::Result;
/// Log the completion of the uninstallation phase.
@ -101,26 +100,20 @@ impl InstallLogger for DefaultInstallLogger {
suffix: Option<&str>,
start: std::time::Instant,
printer: Printer,
dry_run: DryRun,
) -> fmt::Result {
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!(
printer.stderr(),
"{}",
if dry_run.enabled() {
format!("Would download {what}")
} else {
format!(
"Prepared {what} {}",
"Prepared {} {}",
if let Some(suffix) = suffix {
format!("{count} package{s} {suffix}")
} else {
format!("{count} package{s}")
},
format!("in {}", elapsed(start.elapsed())).dimmed()
)
}
.dimmed()
)
}
@ -265,7 +258,6 @@ impl InstallLogger for SummaryInstallLogger {
_suffix: Option<&str>,
_start: std::time::Instant,
_printer: Printer,
_dry_run: DryRun,
) -> fmt::Result {
Ok(())
}
@ -330,7 +322,6 @@ impl InstallLogger for UpgradeInstallLogger {
_suffix: Option<&str>,
_start: std::time::Instant,
_printer: Printer,
_dry_run: DryRun,
) -> fmt::Result {
Ok(())
}

View File

@ -22,7 +22,7 @@ use uv_distribution_types::{
CachedDist, Diagnostic, InstalledDist, LocalDist, NameRequirementSpecification, Requirement,
ResolutionDiagnostic, UnresolvedRequirement, UnresolvedRequirementSpecification,
};
use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution};
use uv_distribution_types::{Name, Resolution};
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_installer::{InstallationStrategy, Plan, Planner, Preparer, SitePackages};
@ -44,9 +44,9 @@ use uv_tool::InstalledTools;
use uv_types::{BuildContext, HashStrategy, InFlight, InstalledPackagesProvider};
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::{ChangeEventKind, DryRunEvent, compile_bytecode};
use crate::printer::Printer;
/// Consolidate the requirements for an installation.
@ -484,11 +484,6 @@ pub(crate) async fn install(
)
.context("Failed to determine installation plan")?;
if dry_run.enabled() {
report_dry_run(dry_run, resolution, plan, modifications, start, printer)?;
return Ok(Changelog::default());
}
let Plan {
cached,
remote,
@ -548,6 +543,7 @@ pub(crate) async fn install(
venv,
logger.as_ref(),
installer_metadata,
dry_run,
printer,
preview,
)
@ -577,6 +573,7 @@ pub(crate) async fn install(
venv,
logger.as_ref(),
installer_metadata,
dry_run,
printer,
preview,
)
@ -595,6 +592,10 @@ pub(crate) async fn install(
// Notify the user of any environment modifications.
logger.on_complete(&changelog, printer, dry_run)?;
if matches!(dry_run, DryRun::Check) {
return Err(Error::OutdatedEnvironment);
}
Ok(changelog)
}
@ -629,6 +630,7 @@ async fn execute_plan(
venv: &PythonEnvironment,
logger: &dyn InstallLogger,
installer_metadata: bool,
dry_run: DryRun,
printer: Printer,
preview: Preview,
) -> Result<(Vec<CachedDist>, Vec<InstalledDist>), Error> {
@ -660,13 +662,7 @@ async fn execute_plan(
.prepare(remote.clone(), in_flight, resolution)
.await?;
logger.on_prepare(
wheels.len(),
phase.map(InstallPhase::label),
start,
printer,
DryRun::Disabled,
)?;
logger.on_prepare(wheels.len(), phase.map(InstallPhase::label), start, printer)?;
wheels
};
@ -676,6 +672,7 @@ async fn execute_plan(
if !uninstalls.is_empty() {
let start = std::time::Instant::now();
if !dry_run.enabled() {
for dist_info in &uninstalls {
match uv_installer::uninstall(dist_info).await {
Ok(summary) => {
@ -707,14 +704,16 @@ async fn execute_plan(
Err(err) => return Err(err.into()),
}
}
}
logger.on_uninstall(uninstalls.len(), start, printer, DryRun::Disabled)?;
logger.on_uninstall(uninstalls.len(), start, printer, dry_run)?;
}
// Install the resolved distributions.
let mut installs = wheels.into_iter().chain(cached).collect::<Vec<_>>();
if !installs.is_empty() {
let start = std::time::Instant::now();
if !dry_run.enabled() {
installs = uv_installer::Installer::new(venv, preview)
.with_link_mode(link_mode)
.with_cache(cache)
@ -726,8 +725,9 @@ async fn execute_plan(
// have no other running tasks at this point, so this lets us avoid spawning a blocking
// task.
.install_blocking(installs)?;
}
logger.on_install(installs.len(), start, printer, DryRun::Disabled)?;
logger.on_install(installs.len(), start, printer, dry_run)?;
}
Ok((installs, uninstalls))
@ -838,116 +838,6 @@ pub(crate) fn report_target_environment(
Ok(writeln!(printer.stderr(), "{}", message.dimmed())?)
}
/// Report on the results of a dry-run installation.
#[allow(clippy::result_large_err)]
fn report_dry_run(
dry_run: DryRun,
resolution: &Resolution,
plan: Plan,
modifications: Modifications,
start: std::time::Instant,
printer: Printer,
) -> Result<(), Error> {
let Plan {
cached,
remote,
reinstalls,
extraneous,
} = plan;
// If we're in `install` mode, ignore any extraneous distributions.
let extraneous = match modifications {
Modifications::Sufficient => vec![],
Modifications::Exact => extraneous,
};
// Nothing to do.
if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() && extraneous.is_empty() {
DefaultInstallLogger.on_audit(resolution.len(), start, printer, dry_run)?;
return Ok(());
}
// Download, build, and unzip any missing distributions.
let wheels = if remote.is_empty() {
vec![]
} else {
DefaultInstallLogger.on_prepare(remote.len(), None, start, printer, dry_run)?;
remote.clone()
};
// Remove any upgraded or extraneous installations.
let uninstalls = extraneous.len() + reinstalls.len();
if uninstalls > 0 {
DefaultInstallLogger.on_uninstall(uninstalls, start, printer, dry_run)?;
}
// Install the resolved distributions.
let installs = wheels.len() + cached.len();
if installs > 0 {
DefaultInstallLogger.on_install(installs, start, printer, dry_run)?;
}
// TODO(charlie): DRY this up with `report_modifications`. The types don't quite line up.
for event in reinstalls
.into_iter()
.chain(extraneous.into_iter())
.map(|distribution| DryRunEvent {
name: distribution.name().clone(),
version: distribution.installed_version().to_string(),
kind: ChangeEventKind::Removed,
})
.chain(wheels.into_iter().map(|distribution| DryRunEvent {
name: distribution.name().clone(),
version: distribution.version_or_url().to_string(),
kind: ChangeEventKind::Added,
}))
.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) {
return Err(Error::OutdatedEnvironment);
}
Ok(())
}
/// Report any diagnostics on resolved distributions.
#[allow(clippy::result_large_err)]
pub(crate) fn diagnose_resolution(

View File

@ -15292,7 +15292,7 @@ fn do_not_simplify_if_not_all_conflict_extras_satisfy_the_marker_by_themselves()
Would use project environment at: .venv
Resolved 4 packages in [TIME]
Found up-to-date lockfile at: uv.lock
Would download 2 packages
Prepared 2 packages in [TIME]
Would install 2 packages
+ python-dateutil==2.8.0
+ six==1.17.0

View File

@ -5322,14 +5322,14 @@ fn dry_run_install() -> std::result::Result<(), Box<dyn std::error::Error>> {
.arg("-r")
.arg("requirements.txt")
.arg("--dry-run")
.arg("--strict"), @r###"
.arg("--strict"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 7 packages in [TIME]
Would download 7 packages
Prepared 7 packages in [TIME]
Would install 7 packages
+ anyio==4.3.0
+ certifi==2024.2.2
@ -5338,7 +5338,7 @@ fn dry_run_install() -> std::result::Result<(), Box<dyn std::error::Error>> {
+ httpx==0.25.1
+ idna==3.6
+ sniffio==1.3.1
"###
"
);
Ok(())
@ -5354,19 +5354,19 @@ fn dry_run_install_url_dependency() -> std::result::Result<(), Box<dyn std::erro
.arg("-r")
.arg("requirements.txt")
.arg("--dry-run")
.arg("--strict"), @r###"
.arg("--strict"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Would download 3 packages
Prepared 3 packages in [TIME]
Would install 3 packages
+ anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz
+ anyio==4.2.0 (from https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz)
+ idna==3.6
+ sniffio==1.3.1
"###
"
);
Ok(())
@ -5405,19 +5405,19 @@ fn dry_run_uninstall_url_dependency() -> std::result::Result<(), Box<dyn std::er
.arg("--upgrade-package")
.arg("anyio")
.arg("--dry-run")
.arg("--strict"), @r###"
.arg("--strict"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Would download 1 package
Prepared 1 package in [TIME]
Would uninstall 1 package
Would install 1 package
- anyio==4.2.0 (from https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz)
+ anyio==4.3.0
"###
"
);
Ok(())
@ -5504,20 +5504,20 @@ fn dry_run_install_transitive_dependency_already_installed()
.arg("-r")
.arg("requirements.txt")
.arg("--dry-run")
.arg("--strict"), @r###"
.arg("--strict"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 7 packages in [TIME]
Would download 4 packages
Prepared 4 packages in [TIME]
Would install 4 packages
+ anyio==4.3.0
+ httpx==0.25.1
+ idna==3.6
+ sniffio==1.3.1
"###
"
);
Ok(())
@ -5557,19 +5557,19 @@ fn dry_run_install_then_upgrade() -> std::result::Result<(), Box<dyn std::error:
uv_snapshot!(context.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--dry-run"), @r###"
.arg("--dry-run"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 7 packages in [TIME]
Would download 1 package
Prepared 1 package in [TIME]
Would uninstall 1 package
Would install 1 package
- httpx==0.25.0
+ httpx==0.25.1
"###
"
);
Ok(())

View File

@ -1056,18 +1056,18 @@ fn warn_on_yanked_dry_run() -> Result<()> {
uv_snapshot!(context.filters(), windows_filters=false, context.pip_sync()
.arg("requirements.txt")
.arg("--dry-run")
.arg("--strict"), @r###"
.arg("--strict"), @r#"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Would download 1 package
Prepared 1 package in [TIME]
Would install 1 package
+ colorama==0.4.2
warning: `colorama==0.4.2` is yanked (reason: "Bad build, missing files, will not install")
"###
"#
);
Ok(())
@ -5883,7 +5883,7 @@ fn pep_751_wheel_only() -> Result<()> {
----- stdout -----
----- stderr -----
Would download 9 packages
Prepared 9 packages in [TIME]
Would install 9 packages
+ filelock==3.13.1
+ fsspec==2024.3.1

View File

@ -641,7 +641,7 @@ fn sync_dry_json() -> Result<()> {
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
Would download 1 package
Prepared 1 package in [TIME]
Would install 1 package
+ iniconfig==2.0.0
"#);
@ -1041,7 +1041,7 @@ fn check() -> Result<()> {
)?;
// Running `uv sync --check` should fail.
uv_snapshot!(context.filters(), context.sync().arg("--check"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--check"), @r"
success: false
exit_code: 1
----- stdout -----
@ -1050,11 +1050,11 @@ fn check() -> Result<()> {
Would use project environment at: .venv
Resolved 2 packages in [TIME]
Would create lockfile at: uv.lock
Would download 1 package
Prepared 1 package in [TIME]
Would install 1 package
+ iniconfig==2.0.0
The environment is outdated; run `uv sync` to update the environment
"###);
");
// Sync the environment.
uv_snapshot!(context.filters(), context.sync(), @r"
@ -1064,7 +1064,6 @@ fn check() -> Result<()> {
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
@ -10588,7 +10587,7 @@ fn sync_dry_run() -> Result<()> {
Would create project environment at: .venv
Resolved 2 packages in [TIME]
Would create lockfile at: uv.lock
Would download 1 package
Prepared 1 package in [TIME]
Would install 1 package
+ iniconfig==2.0.0
");
@ -10603,7 +10602,6 @@ fn sync_dry_run() -> Result<()> {
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
@ -10743,7 +10741,7 @@ fn sync_dry_run_and_locked() -> Result<()> {
----- stderr -----
Would use project environment at: .venv
Resolved 2 packages in [TIME]
Would download 1 package
Prepared 1 package in [TIME]
Would install 1 package
+ iniconfig==2.0.0
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
@ -10794,7 +10792,7 @@ fn sync_dry_run_and_frozen() -> Result<()> {
----- stderr -----
Would use project environment at: .venv
Would download 3 packages
Prepared 3 packages in [TIME]
Would install 3 packages
+ anyio==3.7.0
+ idna==3.6