diff --git a/crates/uv-installer/src/lib.rs b/crates/uv-installer/src/lib.rs index f53972bb0..e342f37d8 100644 --- a/crates/uv-installer/src/lib.rs +++ b/crates/uv-installer/src/lib.rs @@ -3,7 +3,7 @@ pub use downloader::{Downloader, Reporter as DownloadReporter}; pub use editable::{is_dynamic, BuiltEditable, ResolvedEditable}; pub use installer::{Installer, Reporter as InstallReporter}; pub use plan::{Plan, Planner, Reinstall}; -pub use site_packages::SitePackages; +pub use site_packages::{Diagnostic, SitePackages}; pub use uninstall::uninstall; pub use uv_traits::NoBinary; diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index ba71c3dcb..52777b49a 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -67,13 +67,16 @@ impl From for ExitCode { /// Format a duration as a human-readable string, Cargo-style. pub(super) fn elapsed(duration: Duration) -> String { let secs = duration.as_secs(); + let ms = duration.subsec_millis(); if secs >= 60 { format!("{}m {:02}s", secs / 60, secs % 60) } else if secs > 0 { format!("{}.{:02}s", secs, duration.subsec_nanos() / 10_000_000) + } else if ms > 0 { + format!("{ms}ms") } else { - format!("{}ms", duration.subsec_millis()) + format!("0.{:02}ms", duration.subsec_nanos() / 10_000) } } diff --git a/crates/uv/src/commands/pip_check.rs b/crates/uv/src/commands/pip_check.rs index 9cc0056b8..6fc05abc4 100644 --- a/crates/uv/src/commands/pip_check.rs +++ b/crates/uv/src/commands/pip_check.rs @@ -1,24 +1,28 @@ use std::fmt::Write; use anyhow::Result; +use distribution_types::InstalledDist; use owo_colors::OwoColorize; +use std::time::Instant; use tracing::debug; use uv_cache::Cache; use uv_fs::Simplified; -use uv_installer::SitePackages; +use uv_installer::{Diagnostic, SitePackages}; use uv_interpreter::PythonEnvironment; -use crate::commands::ExitStatus; +use crate::commands::{elapsed, ExitStatus}; use crate::printer::Printer; -/// Show information about one or more installed packages. +/// Check for incompatibilties in installed packages. pub(crate) fn pip_check( python: Option<&str>, system: bool, cache: &Cache, printer: Printer, ) -> Result { + let start = Instant::now(); + // Detect the current Python interpreter. let venv = if let Some(python) = python { PythonEnvironment::from_requested_python(python, cache)? @@ -42,18 +46,50 @@ pub(crate) fn pip_check( // Build the installed index. let site_packages = SitePackages::from_executable(&venv)?; + let packages: Vec<&InstalledDist> = site_packages.iter().collect(); - let mut is_compatible = true; - // This loop is entered if and only if there is at least one conflict. - for diagnostic in site_packages.diagnostics()? { - is_compatible = false; - writeln!(printer.stdout(), "{}", diagnostic.message())?; + let s = if packages.len() == 1 { "" } else { "s" }; + writeln!( + printer.stderr(), + "{}", + format!( + "Checked {} in {}", + format!("{} package{}", packages.len(), s).bold(), + elapsed(start.elapsed()) + ) + .dimmed() + )?; + + let diagnostics: Vec = site_packages.diagnostics()?.into_iter().collect(); + + if diagnostics.is_empty() { + writeln!( + printer.stderr(), + "{}", + "All installed packages are compatible".to_string().dimmed() + )?; + + Ok(ExitStatus::Success) + } else { + let incompats = if diagnostics.len() == 1 { + "incompatibility" + } else { + "incompatibilities" + }; + writeln!( + printer.stderr(), + "{}", + format!( + "Found {}", + format!("{} {}", diagnostics.len(), incompats).bold() + ) + .dimmed() + )?; + + for diagnostic in &diagnostics { + writeln!(printer.stderr(), "{}", diagnostic.message().bold())?; + } + + Ok(ExitStatus::Failure) } - - if !is_compatible { - return Ok(ExitStatus::Failure); - } - - writeln!(printer.stdout(), "Installed packages pass the check.").unwrap(); - Ok(ExitStatus::Success) } diff --git a/crates/uv/tests/pip_check.rs b/crates/uv/tests/pip_check.rs index 52895e99f..cd4c13752 100644 --- a/crates/uv/tests/pip_check.rs +++ b/crates/uv/tests/pip_check.rs @@ -32,6 +32,20 @@ fn install_command(context: &TestContext) -> Command { command } +/// Create a `pip check` command with options shared across scenarios. +fn check_command(context: &TestContext) -> Command { + let mut command = Command::new(get_bin()); + command + .arg("pip") + .arg("check") + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .env("VIRTUAL_ENV", context.venv.as_os_str()) + .current_dir(&context.temp_dir); + + command +} + #[test] fn check_compatible_packages() -> Result<()> { let context = TestContext::new("3.12"); @@ -60,20 +74,14 @@ fn check_compatible_packages() -> Result<()> { "### ); - // Guards against the package names being sorted. - uv_snapshot!([], Command::new(get_bin()) - .arg("pip") - .arg("check") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .env("VIRTUAL_ENV", context.venv.as_os_str()) - .current_dir(&context.temp_dir), @r###" + uv_snapshot!(check_command(&context), @r###" success: true exit_code: 0 ----- stdout ----- - Installed packages pass the check. ----- stderr ----- + Checked 5 packages in [TIME] + All installed packages are compatible "### ); @@ -132,20 +140,15 @@ fn check_incompatible_packages() -> Result<()> { "### ); - // Guards against the package names being sorted. - uv_snapshot!([], Command::new(get_bin()) - .arg("pip") - .arg("check") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .env("VIRTUAL_ENV", context.venv.as_os_str()) - .current_dir(&context.temp_dir), @r###" + uv_snapshot!(check_command(&context), @r###" success: false exit_code: 1 ----- stdout ----- - The package `requests` requires `idna <4, >=2.5`, but `2.4` is installed. ----- stderr ----- + Checked 5 packages in [TIME] + Found 1 incompatibility + The package `requests` requires `idna <4, >=2.5`, but `2.4` is installed. "### ); @@ -208,21 +211,16 @@ fn check_multiple_incompatible_packages() -> Result<()> { "### ); - // Guards against the package names being sorted. - uv_snapshot!([], Command::new(get_bin()) - .arg("pip") - .arg("check") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .env("VIRTUAL_ENV", context.venv.as_os_str()) - .current_dir(&context.temp_dir), @r###" + uv_snapshot!(check_command(&context), @r###" success: false exit_code: 1 ----- stdout ----- - The package `requests` requires `idna <4, >=2.5`, but `2.4` is installed. - The package `requests` requires `urllib3 <3, >=1.21.1`, but `1.20` is installed. ----- stderr ----- + Checked 5 packages in [TIME] + Found 2 incompatibilities + The package `requests` requires `idna <4, >=2.5`, but `2.4` is installed. + The package `requests` requires `urllib3 <3, >=1.21.1`, but `1.20` is installed. "### );