diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 9ec38157b..6ec866404 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -27,6 +27,7 @@ use uv_resolver::{ExcludeNewer, PrereleaseMode, RequiresPython}; use crate::commands::pip::latest::LatestClient; use crate::commands::pip::operations::report_target_environment; +use crate::commands::reporters::LatestVersionReporter; use crate::commands::ExitStatus; use crate::printer::Printer; @@ -78,7 +79,7 @@ pub(crate) async fn pip_list( .collect_vec(); // Determine the latest version for each package. - let latest = if outdated { + let latest = if outdated && !results.is_empty() { let capabilities = IndexCapabilities::default(); // Initialize the registry client. @@ -110,6 +111,8 @@ pub(crate) async fn pip_list( requires_python: &requires_python, }; + let reporter = LatestVersionReporter::from(printer).with_length(results.len() as u64); + // Fetch the latest version for each package. let mut fetches = futures::stream::iter(&results) .map(|dist| async { @@ -120,8 +123,14 @@ pub(crate) async fn pip_list( let mut map = FxHashMap::default(); while let Some((package, version)) = fetches.next().await.transpose()? { + if let Some(version) = version.as_ref() { + reporter.on_fetch_version(package, version.version()); + } else { + reporter.on_fetch_progress(); + } map.insert(package, version); } + reporter.on_fetch_complete(); map } else { FxHashMap::default() diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 599b30a69..3e926a313 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -24,6 +24,7 @@ use crate::commands::project::lock::{do_safe_lock, LockMode}; use crate::commands::project::{ default_dependency_groups, DependencyGroupsTarget, ProjectError, ProjectInterpreter, }; +use crate::commands::reporters::LatestVersionReporter; use crate::commands::{diagnostics, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverSettings; @@ -142,73 +143,89 @@ pub(crate) async fn tree( // If necessary, look up the latest version of each package. let latest = if outdated { - let ResolverSettings { - index_locations: _, - index_strategy: _, - keyring_provider, - resolution: _, - prerelease: _, - dependency_metadata: _, - config_setting: _, - no_build_isolation: _, - no_build_isolation_package: _, - exclude_newer: _, - link_mode: _, - upgrade: _, - build_options: _, - sources: _, - } = &settings; + // Filter to packages that are derived from a registry. + let packages = lock + .packages() + .iter() + .filter_map(|package| { + let index = match package.index(workspace.install_path()) { + Ok(Some(index)) => index, + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; + Some(Ok((package, index))) + }) + .collect::, _>>()?; - let capabilities = IndexCapabilities::default(); + if packages.is_empty() { + PackageMap::default() + } else { + let ResolverSettings { + index_locations: _, + index_strategy: _, + keyring_provider, + resolution: _, + prerelease: _, + dependency_metadata: _, + config_setting: _, + no_build_isolation: _, + no_build_isolation_package: _, + exclude_newer: _, + link_mode: _, + upgrade: _, + build_options: _, + sources: _, + } = &settings; - // Initialize the registry client. - let client = - RegistryClientBuilder::new(cache.clone().with_refresh(Refresh::All(Timestamp::now()))) - .native_tls(native_tls) - .connectivity(connectivity) - .keyring(*keyring_provider) - .allow_insecure_host(allow_insecure_host.to_vec()) - .build(); + let capabilities = IndexCapabilities::default(); - // Initialize the client to fetch the latest version of each package. - let client = LatestClient { - client: &client, - capabilities: &capabilities, - prerelease: lock.prerelease_mode(), - exclude_newer: lock.exclude_newer(), - requires_python: lock.requires_python(), - tags: None, - }; + // Initialize the registry client. + let client = RegistryClientBuilder::new( + cache.clone().with_refresh(Refresh::All(Timestamp::now())), + ) + .native_tls(native_tls) + .connectivity(connectivity) + .keyring(*keyring_provider) + .allow_insecure_host(allow_insecure_host.to_vec()) + .build(); - // Fetch the latest version for each package. - let mut fetches = futures::stream::iter(lock.packages().iter().filter_map(|package| { - // Filter to packages that are derived from a registry. - let index = match package.index(workspace.install_path()) { - Ok(Some(index)) => index, - Ok(None) => return None, - Err(err) => return Some(Err(err)), + // Initialize the client to fetch the latest version of each package. + let client = LatestClient { + client: &client, + capabilities: &capabilities, + prerelease: lock.prerelease_mode(), + exclude_newer: lock.exclude_newer(), + requires_python: lock.requires_python(), + tags: None, }; - Some(Ok((package, index))) - })) - .map(|result| async move { - let (package, index) = result?; - let Some(filename) = client.find_latest(package.name(), Some(&index)).await? else { - return Ok(None); - }; - if filename.version() == package.version() { - return Ok(None); - } - Ok::, Error>(Some((package, filename.into_version()))) - }) - .buffer_unordered(concurrency.downloads); - let mut map = PackageMap::default(); - while let Some(entry) = fetches.next().await.transpose()? { - if let Some((package, version)) = entry { - map.insert(package.clone(), version); + let reporter = LatestVersionReporter::from(printer).with_length(packages.len() as u64); + + // Fetch the latest version for each package. + let mut fetches = futures::stream::iter(packages) + .map(|(package, index)| async move { + let Some(filename) = client.find_latest(package.name(), Some(&index)).await? + else { + return Ok(None); + }; + Ok::, Error>(Some((package, filename.into_version()))) + }) + .buffer_unordered(concurrency.downloads); + + let mut map = PackageMap::default(); + while let Some(entry) = fetches.next().await.transpose()? { + let Some((package, version)) = entry else { + reporter.on_fetch_progress(); + continue; + }; + reporter.on_fetch_version(package.name(), &version); + if version > *package.version() { + map.insert(package.clone(), version); + } } + reporter.on_fetch_complete(); + map } - map } else { PackageMap::default() }; diff --git a/crates/uv/src/commands/reporters.rs b/crates/uv/src/commands/reporters.rs index 69982115d..0ceef7f8d 100644 --- a/crates/uv/src/commands/reporters.rs +++ b/crates/uv/src/commands/reporters.rs @@ -11,6 +11,7 @@ use uv_distribution_types::{ BuildableSource, CachedDist, DistributionMetadata, Name, SourceDist, VersionOrUrlRef, }; use uv_normalize::PackageName; +use uv_pep440::Version; use uv_python::PythonInstallationKey; use uv_static::EnvVars; @@ -528,6 +529,44 @@ impl uv_publish::Reporter for PublishReporter { } } +#[derive(Debug)] +pub(crate) struct LatestVersionReporter { + progress: ProgressBar, +} + +impl From for LatestVersionReporter { + fn from(printer: Printer) -> Self { + let progress = ProgressBar::with_draw_target(None, printer.target()); + progress.set_style( + ProgressStyle::with_template("{bar:20} [{pos}/{len}] {wide_msg:.dim}").unwrap(), + ); + progress.set_message("Fetching latest versions..."); + Self { progress } + } +} + +impl LatestVersionReporter { + #[must_use] + pub(crate) fn with_length(self, length: u64) -> Self { + self.progress.set_length(length); + self + } + + pub(crate) fn on_fetch_progress(&self) { + self.progress.inc(1); + } + + pub(crate) fn on_fetch_version(&self, name: &PackageName, version: &Version) { + self.progress.set_message(format!("{name} v{version}")); + self.progress.inc(1); + } + + pub(crate) fn on_fetch_complete(&self) { + self.progress.set_message(""); + self.progress.finish_and_clear(); + } +} + #[derive(Debug)] pub(crate) struct CleaningDirectoryReporter { bar: ProgressBar,