Add progress bar when downloading python (#4840)

## Summary

Resolves #4825 

## Test Plan

```sh
$ cargo run -- python install --force --preview
$ cargo run -- venv -p 3.12 --python-preference only-managed
$ cargo run -- tool install --preview -p 3.12 --python-preference only-managed --force black
````

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Jo 2024-07-08 04:01:35 +08:00 committed by GitHub
parent 9e50864508
commit f4c4b69cc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 220 additions and 59 deletions

1
Cargo.lock generated
View file

@ -4927,6 +4927,7 @@ dependencies = [
"tempfile", "tempfile",
"test-log", "test-log",
"thiserror", "thiserror",
"tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
"url", "url",

View file

@ -31,8 +31,8 @@ anyhow = { workspace = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
configparser = { workspace = true } configparser = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] }
itertools = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
itertools = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }
@ -45,6 +45,7 @@ serde_json = { workspace = true }
target-lexicon = { workspace = true } target-lexicon = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true, features = ["compat"] } tokio-util = { workspace = true, features = ["compat"] }
tracing = { workspace = true } tracing = { workspace = true }
url = { workspace = true } url = { workspace = true }

View file

@ -1,7 +1,21 @@
use std::fmt::Display; use std::fmt::Display;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::str::FromStr; use std::str::FromStr;
use std::task::{Context, Poll};
use futures::TryStreamExt;
use thiserror::Error;
use tokio::io::{AsyncRead, ReadBuf};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::{debug, instrument};
use url::Url;
use pypi_types::{HashAlgorithm, HashDigest};
use uv_client::WrappedReqwestError;
use uv_extract::hash::Hasher;
use uv_fs::{rename_with_retry, Simplified};
use crate::implementation::{ use crate::implementation::{
Error as ImplementationError, ImplementationName, LenientImplementationName, Error as ImplementationError, ImplementationName, LenientImplementationName,
@ -9,17 +23,6 @@ use crate::implementation::{
use crate::installation::PythonInstallationKey; use crate::installation::PythonInstallationKey;
use crate::platform::{self, Arch, Libc, Os}; use crate::platform::{self, Arch, Libc, Os};
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest}; use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
use thiserror::Error;
use uv_client::WrappedReqwestError;
use futures::TryStreamExt;
use pypi_types::{HashAlgorithm, HashDigest};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::{debug, instrument};
use url::Url;
use uv_extract::hash::Hasher;
use uv_fs::{rename_with_retry, Simplified};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@ -400,11 +403,12 @@ impl ManagedPythonDownload {
} }
/// Download and extract /// Download and extract
#[instrument(skip(client, parent_path), fields(download = %self.key()))] #[instrument(skip(client, parent_path, reporter), fields(download = % self.key()))]
pub async fn fetch( pub async fn fetch(
&self, &self,
client: &uv_client::BaseClient, client: &uv_client::BaseClient,
parent_path: &Path, parent_path: &Path,
reporter: Option<&dyn Reporter>,
) -> Result<DownloadResult, Error> { ) -> Result<DownloadResult, Error> {
let url = Url::parse(self.url)?; let url = Url::parse(self.url)?;
let path = parent_path.join(self.key().to_string()); let path = parent_path.join(self.key().to_string());
@ -420,6 +424,11 @@ impl ManagedPythonDownload {
// Ensure the request was successful. // Ensure the request was successful.
response.error_for_status_ref()?; response.error_for_status_ref()?;
let size = response.content_length();
let progress = reporter
.as_ref()
.map(|reporter| (reporter, reporter.on_download_start(&self.key, size)));
// Download and extract into a temporary directory. // Download and extract into a temporary directory.
let temp_dir = tempfile::tempdir_in(parent_path).map_err(Error::DownloadDirError)?; let temp_dir = tempfile::tempdir_in(parent_path).map_err(Error::DownloadDirError)?;
@ -427,28 +436,44 @@ impl ManagedPythonDownload {
"Downloading {url} to temporary location {}", "Downloading {url} to temporary location {}",
temp_dir.path().display() temp_dir.path().display()
); );
let reader = response
let stream = response
.bytes_stream() .bytes_stream()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err)) .map_err(|err| io::Error::new(io::ErrorKind::Other, err))
.into_async_read(); .into_async_read();
let mut hashers = self
.sha256
.into_iter()
.map(|_| Hasher::from(HashAlgorithm::Sha256))
.collect::<Vec<_>>();
let mut hasher = uv_extract::hash::HashReader::new(stream.compat(), &mut hashers);
debug!("Extracting {filename}"); debug!("Extracting {filename}");
match progress {
Some((&reporter, progress)) => {
let mut reader = ProgressReader::new(&mut hasher, progress, reporter);
uv_extract::stream::archive(&mut reader, filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
}
None => {
uv_extract::stream::archive(&mut hasher, filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
}
};
hasher.finish().await.map_err(Error::HashExhaustion)?;
if let Some((&reporter, progress)) = progress {
reporter.on_progress(&self.key, progress);
}
// Check the hash
if let Some(expected) = self.sha256 { if let Some(expected) = self.sha256 {
let mut hashers = [Hasher::from(HashAlgorithm::Sha256)]; let actual = HashDigest::from(hashers.pop().unwrap()).digest;
let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
uv_extract::stream::archive(&mut hasher, filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
hasher.finish().await.map_err(Error::HashExhaustion)?;
let actual = hashers
.into_iter()
.map(HashDigest::from)
.next()
.unwrap()
.digest;
if !actual.eq_ignore_ascii_case(expected) { if !actual.eq_ignore_ascii_case(expected) {
return Err(Error::HashMismatch { return Err(Error::HashMismatch {
installation: self.key.to_string(), installation: self.key.to_string(),
@ -456,10 +481,6 @@ impl ManagedPythonDownload {
actual: actual.to_string(), actual: actual.to_string(),
}); });
} }
} else {
uv_extract::stream::archive(reader.compat(), filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
} }
// Extract the top-level directory. // Extract the top-level directory.
@ -513,3 +534,46 @@ impl Display for ManagedPythonDownload {
write!(f, "{}", self.key) write!(f, "{}", self.key)
} }
} }
pub trait Reporter: Send + Sync {
fn on_progress(&self, name: &PythonInstallationKey, id: usize);
fn on_download_start(&self, name: &PythonInstallationKey, size: Option<u64>) -> usize;
fn on_download_progress(&self, id: usize, inc: u64);
fn on_download_complete(&self);
}
/// An asynchronous reader that reports progress as bytes are read.
struct ProgressReader<'a, R> {
reader: R,
index: usize,
reporter: &'a dyn Reporter,
}
impl<'a, R> ProgressReader<'a, R> {
/// Create a new [`ProgressReader`] that wraps another reader.
fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
Self {
reader,
index,
reporter,
}
}
}
impl<R> AsyncRead for ProgressReader<'_, R>
where
R: AsyncRead + Unpin,
{
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.as_mut().reader)
.poll_read(cx, buf)
.map_ok(|()| {
self.reporter
.on_download_progress(self.index, buf.filled().len() as u64);
})
}
}

View file

@ -10,7 +10,7 @@ use uv_cache::Cache;
use crate::discovery::{ use crate::discovery::{
find_best_python_installation, find_python_installation, EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation, EnvironmentPreference, PythonRequest,
}; };
use crate::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest}; use crate::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest, Reporter};
use crate::implementation::LenientImplementationName; use crate::implementation::LenientImplementationName;
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations}; use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
use crate::platform::{Arch, Libc, Os}; use crate::platform::{Arch, Libc, Os};
@ -82,13 +82,14 @@ impl PythonInstallation {
python_fetch: PythonFetch, python_fetch: PythonFetch,
client_builder: &BaseClientBuilder<'a>, client_builder: &BaseClientBuilder<'a>,
cache: &Cache, cache: &Cache,
reporter: Option<&dyn Reporter>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let request = request.unwrap_or_default(); let request = request.unwrap_or_default();
// Perform a fetch aggressively if managed Python is preferred // Perform a fetch aggressively if managed Python is preferred
if matches!(preference, PythonPreference::Managed) && python_fetch.is_automatic() { if matches!(preference, PythonPreference::Managed) && python_fetch.is_automatic() {
if let Some(request) = PythonDownloadRequest::try_from_request(&request) { if let Some(request) = PythonDownloadRequest::try_from_request(&request) {
return Self::fetch(request.fill(), client_builder, cache).await; return Self::fetch(request.fill(), client_builder, cache, reporter).await;
} }
} }
@ -103,7 +104,7 @@ impl PythonInstallation {
{ {
if let Some(request) = PythonDownloadRequest::try_from_request(&request) { if let Some(request) = PythonDownloadRequest::try_from_request(&request) {
debug!("Requested Python not found, checking for available download..."); debug!("Requested Python not found, checking for available download...");
Self::fetch(request.fill(), client_builder, cache).await Self::fetch(request.fill(), client_builder, cache, reporter).await
} else { } else {
err err
} }
@ -117,6 +118,7 @@ impl PythonInstallation {
request: PythonDownloadRequest, request: PythonDownloadRequest,
client_builder: &BaseClientBuilder<'a>, client_builder: &BaseClientBuilder<'a>,
cache: &Cache, cache: &Cache,
reporter: Option<&dyn Reporter>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let installations = ManagedPythonInstallations::from_settings()?.init()?; let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations_dir = installations.root(); let installations_dir = installations.root();
@ -126,7 +128,7 @@ impl PythonInstallation {
let client = client_builder.build(); let client = client_builder.build();
info!("Fetching requested Python..."); info!("Fetching requested Python...");
let result = download.fetch(&client, installations_dir).await?; let result = download.fetch(&client, installations_dir, reporter).await?;
let path = match result { let path = match result {
DownloadResult::AlreadyAvailable(path) => path, DownloadResult::AlreadyAvailable(path) => path,

View file

@ -7,7 +7,7 @@ pub use crate::discovery::{
}; };
pub use crate::environment::PythonEnvironment; pub use crate::environment::PythonEnvironment;
pub use crate::implementation::ImplementationName; pub use crate::implementation::ImplementationName;
pub use crate::installation::PythonInstallation; pub use crate::installation::{PythonInstallation, PythonInstallationKey};
pub use crate::interpreter::{Error as InterpreterError, Interpreter}; pub use crate::interpreter::{Error as InterpreterError, Interpreter};
pub use crate::pointer_size::PointerSize; pub use crate::pointer_size::PointerSize;
pub use crate::prefix::Prefix; pub use crate::prefix::Prefix;

View file

@ -25,7 +25,7 @@ use uv_resolver::{FlatIndex, OptionsBuilder, PythonRequirement, RequiresPython,
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::reporters::ResolverReporter; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{pip, SharedState}; use crate::commands::{pip, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings, ResolverSettingsRef}; use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings, ResolverSettingsRef};
@ -180,6 +180,8 @@ impl FoundInterpreter {
.connectivity(connectivity) .connectivity(connectivity)
.native_tls(native_tls); .native_tls(native_tls);
let reporter = PythonDownloadReporter::single(printer);
// Locate the Python interpreter to use in the environment // Locate the Python interpreter to use in the environment
let interpreter = PythonInstallation::find_or_fetch( let interpreter = PythonInstallation::find_or_fetch(
python_request, python_request,
@ -188,6 +190,7 @@ impl FoundInterpreter {
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await? .await?
.into_interpreter(); .into_interpreter();

View file

@ -22,6 +22,7 @@ use uv_warnings::warn_user_once;
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::environment::CachedEnvironment;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{project, ExitStatus, SharedState}; use crate::commands::{project, ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings; use crate::settings::ResolverInstallerSettings;
@ -55,6 +56,8 @@ pub(crate) async fn run(
// Initialize any shared state. // Initialize any shared state.
let state = SharedState::default(); let state = SharedState::default();
let reporter = PythonDownloadReporter::single(printer);
// Determine whether the command to execute is a PEP 723 script. // Determine whether the command to execute is a PEP 723 script.
let script_interpreter = if let RunCommand::Python(target, _) = &command { let script_interpreter = if let RunCommand::Python(target, _) = &command {
if let Some(metadata) = uv_scripts::read_pep723_metadata(&target).await? { if let Some(metadata) = uv_scripts::read_pep723_metadata(&target).await? {
@ -84,6 +87,7 @@ pub(crate) async fn run(
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await? .await?
.into_interpreter(); .into_interpreter();
@ -214,6 +218,7 @@ pub(crate) async fn run(
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await?; .await?;
@ -256,6 +261,7 @@ pub(crate) async fn run(
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await? .await?
.into_interpreter() .into_interpreter()

View file

@ -14,6 +14,7 @@ use uv_python::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
use uv_python::{requests_from_version_file, PythonRequest}; use uv_python::{requests_from_version_file, PythonRequest};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
use crate::printer::Printer; use crate::printer::Printer;
@ -128,17 +129,20 @@ pub(crate) async fn install(
.native_tls(native_tls) .native_tls(native_tls)
.build(); .build();
let mut tasks = futures::stream::iter(downloads.iter()) let reporter = PythonDownloadReporter::new(printer, downloads.len() as u64);
let results = futures::stream::iter(downloads.iter())
.map(|download| async { .map(|download| async {
let _ = writeln!(printer.stderr(), "Downloading {}", download.key()); let result = download
let result = download.fetch(&client, installations_dir).await; .fetch(&client, installations_dir, Some(&reporter))
.await;
(download.python_version(), result) (download.python_version(), result)
}) })
.buffered(4); .buffered(4)
.collect::<Vec<_>>()
.await;
let mut results = Vec::new(); for (version, result) in results {
while let Some(task) = tasks.next().await {
let (version, result) = task;
let path = match result? { let path = match result? {
// We should only encounter already-available during concurrent installs // We should only encounter already-available during concurrent installs
DownloadResult::AlreadyAvailable(path) => path, DownloadResult::AlreadyAvailable(path) => path,
@ -151,10 +155,10 @@ pub(crate) async fn install(
path path
} }
}; };
// Ensure the installations have externally managed markers // Ensure the installations have externally managed markers
let installed = ManagedPythonInstallation::new(path.clone())?; let installed = ManagedPythonInstallation::new(path.clone())?;
installed.ensure_externally_managed()?; installed.ensure_externally_managed()?;
results.push((version, path));
} }
let s = if downloads.len() == 1 { "" } else { "s" }; let s = if downloads.len() == 1 { "" } else { "s" };

View file

@ -11,6 +11,7 @@ use distribution_types::{
BuildableSource, CachedDist, DistributionMetadata, Name, SourceDist, VersionOrUrlRef, BuildableSource, CachedDist, DistributionMetadata, Name, SourceDist, VersionOrUrlRef,
}; };
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_python::PythonInstallationKey;
use crate::printer::Printer; use crate::printer::Printer;
@ -23,9 +24,9 @@ struct ProgressReporter {
#[derive(Debug)] #[derive(Debug)]
enum ProgressMode { enum ProgressMode {
// Reports top-level progress. /// Reports top-level progress.
Single, Single,
// Reports progress of all concurrent download/build/checkout processes. /// Reports progress of all concurrent download, build, and checkout processes.
Multi { Multi {
multi_progress: MultiProgress, multi_progress: MultiProgress,
state: Arc<Mutex<BarState>>, state: Arc<Mutex<BarState>>,
@ -34,18 +35,18 @@ enum ProgressMode {
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct BarState { struct BarState {
// The number of bars that precede any download bars (i.e. build/checkout status). /// The number of bars that precede any download bars (i.e. build/checkout status).
headers: usize, headers: usize,
// A list of donwnload bar sizes, in descending order. /// A list of download bar sizes, in descending order.
sizes: Vec<u64>, sizes: Vec<u64>,
// A map of progress bars, by ID. /// A map of progress bars, by ID.
bars: FxHashMap<usize, ProgressBar>, bars: FxHashMap<usize, ProgressBar>,
// A monotonic counter for bar IDs. /// A monotonic counter for bar IDs.
id: usize, id: usize,
} }
impl BarState { impl BarState {
// Returns a unique ID for a new bar. /// Returns a unique ID for a new progress bar.
fn id(&mut self) -> usize { fn id(&mut self) -> usize {
self.id += 1; self.id += 1;
self.id self.id
@ -72,6 +73,7 @@ impl ProgressReporter {
mode, mode,
} }
} }
fn on_build_start(&self, source: &BuildableSource) -> usize { fn on_build_start(&self, source: &BuildableSource) -> usize {
let ProgressMode::Multi { let ProgressMode::Multi {
multi_progress, multi_progress,
@ -119,7 +121,7 @@ impl ProgressReporter {
)); ));
} }
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize { fn on_download_start(&self, name: String, size: Option<u64>) -> usize {
let ProgressMode::Multi { let ProgressMode::Multi {
multi_progress, multi_progress,
state, state,
@ -148,10 +150,10 @@ impl ProgressReporter {
.unwrap() .unwrap()
.progress_chars("--"), .progress_chars("--"),
); );
progress.set_message(name.to_string()); progress.set_message(name);
} else { } else {
progress.set_style(ProgressStyle::with_template("{wide_msg:.dim} ....").unwrap()); progress.set_style(ProgressStyle::with_template("{wide_msg:.dim} ....").unwrap());
progress.set_message(name.to_string()); progress.set_message(name);
progress.finish(); progress.finish();
} }
@ -279,7 +281,7 @@ impl uv_installer::PrepareReporter for PrepareReporter {
} }
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize { fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
self.reporter.on_download_start(name, size) self.reporter.on_download_start(name.to_string(), size)
} }
fn on_download_progress(&self, id: usize, bytes: u64) { fn on_download_progress(&self, id: usize, bytes: u64) {
@ -363,7 +365,7 @@ impl uv_resolver::ResolverReporter for ResolverReporter {
} }
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize { fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
self.reporter.on_download_start(name, size) self.reporter.on_download_start(name.to_string(), size)
} }
fn on_download_progress(&self, id: usize, bytes: u64) { fn on_download_progress(&self, id: usize, bytes: u64) {
@ -385,7 +387,7 @@ impl uv_distribution::Reporter for ResolverReporter {
} }
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize { fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
self.reporter.on_download_start(name, size) self.reporter.on_download_start(name.to_string(), size)
} }
fn on_download_progress(&self, id: usize, bytes: u64) { fn on_download_progress(&self, id: usize, bytes: u64) {
@ -441,6 +443,72 @@ impl uv_installer::InstallReporter for InstallReporter {
} }
} }
#[derive(Debug)]
pub(crate) struct PythonDownloadReporter {
reporter: ProgressReporter,
multiple: bool,
}
impl PythonDownloadReporter {
/// Initialize a [`PythonDownloadReporter`] for a single Python download.
pub(crate) fn single(printer: Printer) -> Self {
Self::new(printer, 1)
}
/// Initialize a [`PythonDownloadReporter`] for multiple Python downloads.
pub(crate) fn new(printer: Printer, length: u64) -> Self {
let multi_progress = MultiProgress::with_draw_target(printer.target());
let root = multi_progress.add(ProgressBar::with_draw_target(
Some(length),
printer.target(),
));
root.set_style(
ProgressStyle::with_template("{bar:20} [{pos}/{len}] {wide_msg:.dim}").unwrap(),
);
let reporter = ProgressReporter::new(root, multi_progress, printer);
Self {
reporter,
multiple: length > 1,
}
}
}
impl uv_python::downloads::Reporter for PythonDownloadReporter {
fn on_progress(&self, _name: &PythonInstallationKey, id: usize) {
self.reporter.on_download_complete(id);
if self.multiple {
self.reporter.root.inc(1);
if self
.reporter
.root
.length()
.is_some_and(|len| self.reporter.root.position() == len)
{
self.reporter.root.finish_and_clear();
}
}
}
fn on_download_start(&self, name: &PythonInstallationKey, size: Option<u64>) -> usize {
if self.multiple {
self.reporter.root.set_message("Downloading Python...");
}
self.reporter.on_download_start(name.to_string(), size)
}
fn on_download_progress(&self, id: usize, inc: u64) {
self.reporter.on_download_progress(id, inc);
}
fn on_download_complete(&self) {
self.reporter.root.set_message("");
self.reporter.root.finish_and_clear();
}
}
/// Like [`std::fmt::Display`], but with colors. /// Like [`std::fmt::Display`], but with colors.
trait ColorDisplay { trait ColorDisplay {
fn to_color_string(&self) -> String; fn to_color_string(&self) -> String;

View file

@ -25,6 +25,7 @@ use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool,
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::commands::project::{resolve_environment, sync_environment, update_environment}; use crate::commands::project::{resolve_environment, sync_environment, update_environment};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::tool::common::resolve_requirements; use crate::commands::tool::common::resolve_requirements;
use crate::commands::{ExitStatus, SharedState}; use crate::commands::{ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
@ -55,6 +56,8 @@ pub(crate) async fn install(
.connectivity(connectivity) .connectivity(connectivity)
.native_tls(native_tls); .native_tls(native_tls);
let reporter = PythonDownloadReporter::single(printer);
let python_request = python.as_deref().map(PythonRequest::parse); let python_request = python.as_deref().map(PythonRequest::parse);
// Pre-emptively identify a Python interpreter. We need an interpreter to resolve any unnamed // Pre-emptively identify a Python interpreter. We need an interpreter to resolve any unnamed
@ -66,6 +69,7 @@ pub(crate) async fn install(
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await? .await?
.into_interpreter(); .into_interpreter();

View file

@ -24,6 +24,7 @@ use uv_tool::InstalledTools;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::environment::CachedEnvironment;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::tool::common::resolve_requirements; use crate::commands::tool::common::resolve_requirements;
use crate::commands::{ExitStatus, SharedState}; use crate::commands::{ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
@ -154,6 +155,8 @@ async fn get_or_create_environment(
.connectivity(connectivity) .connectivity(connectivity)
.native_tls(native_tls); .native_tls(native_tls);
let reporter = PythonDownloadReporter::single(printer);
let python_request = python.map(PythonRequest::parse); let python_request = python.map(PythonRequest::parse);
// Discover an interpreter. // Discover an interpreter.
@ -164,6 +167,7 @@ async fn get_or_create_environment(
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await? .await?
.into_interpreter(); .into_interpreter();

View file

@ -28,6 +28,7 @@ use uv_python::{
use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_resolver::{ExcludeNewer, FlatIndex};
use uv_types::{BuildContext, BuildIsolation, HashStrategy}; use uv_types::{BuildContext, BuildIsolation, HashStrategy};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{pip, ExitStatus, SharedState}; use crate::commands::{pip, ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
use crate::shell::Shell; use crate::shell::Shell;
@ -131,6 +132,8 @@ async fn venv_impl(
let client_builder_clone = client_builder.clone(); let client_builder_clone = client_builder.clone();
let reporter = PythonDownloadReporter::single(printer);
let mut interpreter_request = python_request.map(PythonRequest::parse); let mut interpreter_request = python_request.map(PythonRequest::parse);
if preview.is_enabled() && interpreter_request.is_none() { if preview.is_enabled() && interpreter_request.is_none() {
interpreter_request = request_from_version_file().await.into_diagnostic()?; interpreter_request = request_from_version_file().await.into_diagnostic()?;
@ -144,6 +147,7 @@ async fn venv_impl(
python_fetch, python_fetch,
&client_builder, &client_builder,
cache, cache,
Some(&reporter),
) )
.await .await
.into_diagnostic()? .into_diagnostic()?