diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 658a7ad553ca..94ab6ef878f1 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4225,8 +4225,20 @@ pub struct PythonListArgs { /// Only show installed Python versions, exclude available downloads. /// /// By default, available downloads for the current platform are shown. - #[arg(long)] + #[arg(long, conflicts_with("only_downloads"))] pub only_installed: bool, + + /// Only show Python downloads, exclude installed distributions. + /// + /// By default, available downloads for the current platform are shown. + #[arg(long, conflicts_with("only_installed"))] + pub only_downloads: bool, + + /// Show the URLs of available Python downloads. + /// + /// By default, these display as ``. + #[arg(long)] + pub show_urls: bool, } #[derive(Args)] diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 42a8128b720a..0c859770bcb4 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -2,6 +2,7 @@ use std::collections::BTreeSet; use std::fmt::Write; use anyhow::Result; +use itertools::Either; use owo_colors::OwoColorize; use rustc_hash::FxHashSet; use uv_cache::Cache; @@ -29,6 +30,7 @@ pub(crate) async fn list( kinds: PythonListKinds, all_versions: bool, all_platforms: bool, + show_urls: bool, python_preference: PythonPreference, python_downloads: PythonDownloads, cache: &Cache, @@ -38,6 +40,11 @@ pub(crate) async fn list( if python_preference != PythonPreference::OnlySystem { let download_request = match kinds { PythonListKinds::Installed => None, + PythonListKinds::Downloads => Some(if all_platforms { + PythonDownloadRequest::default() + } else { + PythonDownloadRequest::from_env()? + }), PythonListKinds::Default => { if python_downloads.is_automatic() { Some(if all_platforms { @@ -61,48 +68,60 @@ pub(crate) async fn list( .flatten(); for download in downloads { - output.insert((download.key().clone(), Kind::Download, None)); + output.insert(( + download.key().clone(), + Kind::Download, + Either::Right(download.url()), + )); } }; - let installed = find_python_installations( - &PythonRequest::Any, - EnvironmentPreference::OnlySystem, - python_preference, - cache, - ) - // Raise discovery errors if critical - .filter(|result| { - result - .as_ref() - .err() - .map_or(true, DiscoveryError::is_critical) - }) - .collect::>, DiscoveryError>>()? - .into_iter() - // Drop any "missing" installations - .filter_map(Result::ok); - - for installation in installed { - let kind = if matches!(installation.source(), PythonSource::Managed) { - Kind::Managed - } else { - Kind::System + let installed = + match kinds { + PythonListKinds::Installed | PythonListKinds::Default => { + Some(find_python_installations( + &PythonRequest::Any, + EnvironmentPreference::OnlySystem, + python_preference, + cache, + ) + // Raise discovery errors if critical + .filter(|result| { + result + .as_ref() + .err() + .map_or(true, DiscoveryError::is_critical) + }) + .collect::>, DiscoveryError>>()? + .into_iter() + // Drop any "missing" installations + .filter_map(Result::ok)) + } + PythonListKinds::Downloads => None, }; - output.insert(( - installation.key(), - kind, - Some(installation.interpreter().sys_executable().to_path_buf()), - )); + + if let Some(installed) = installed { + for installation in installed { + let kind = if matches!(installation.source(), PythonSource::Managed) { + Kind::Managed + } else { + Kind::System + }; + output.insert(( + installation.key(), + kind, + Either::Left(installation.interpreter().sys_executable().to_path_buf()), + )); + } } let mut seen_minor = FxHashSet::default(); let mut seen_patch = FxHashSet::default(); let mut seen_paths = FxHashSet::default(); let mut include = Vec::new(); - for (key, kind, path) in output.iter().rev() { + for (key, kind, uri) in output.iter().rev() { // Do not show the same path more than once - if let Some(path) = path { + if let Either::Left(path) = uri { if !seen_paths.insert(path) { continue; } @@ -142,7 +161,7 @@ pub(crate) async fn list( } } } - include.push((key, path)); + include.push((key, uri)); } // Compute the width of the first column. @@ -150,30 +169,37 @@ pub(crate) async fn list( .iter() .fold(0usize, |acc, (key, _)| acc.max(key.to_string().len())); - for (key, path) in include { + for (key, uri) in include { let key = key.to_string(); - if let Some(path) = path { - let is_symlink = fs_err::symlink_metadata(path)?.is_symlink(); - if is_symlink { - writeln!( - printer.stdout(), - "{key:width$} {} -> {}", - path.user_display().cyan(), - path.read_link()?.user_display().cyan() - )?; - } else { - writeln!( - printer.stdout(), - "{key:width$} {}", - path.user_display().cyan() - )?; + match uri { + Either::Left(path) => { + let is_symlink = fs_err::symlink_metadata(path)?.is_symlink(); + if is_symlink { + writeln!( + printer.stdout(), + "{key:width$} {} -> {}", + path.user_display().cyan(), + path.read_link()?.user_display().cyan() + )?; + } else { + writeln!( + printer.stdout(), + "{key:width$} {}", + path.user_display().cyan() + )?; + } + } + Either::Right(url) => { + if show_urls { + writeln!(printer.stdout(), "{key:width$} {}", url.dimmed())?; + } else { + writeln!( + printer.stdout(), + "{key:width$} {}", + "".dimmed() + )?; + } } - } else { - writeln!( - printer.stdout(), - "{key:width$} {}", - "".dimmed() - )?; } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 5cfac3ef7fc0..029f772ab7d3 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1088,6 +1088,7 @@ async fn run(mut cli: Cli) -> Result { args.kinds, args.all_versions, args.all_platforms, + args.show_urls, globals.python_preference, globals.python_downloads, &cache, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 58d9711dac80..ada9e69ae8a8 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -703,6 +703,9 @@ impl ToolDirSettings { pub(crate) enum PythonListKinds { #[default] Default, + /// Only list version downloads. + Downloads, + /// Only list installed versions. Installed, } @@ -713,6 +716,7 @@ pub(crate) struct PythonListSettings { pub(crate) kinds: PythonListKinds, pub(crate) all_platforms: bool, pub(crate) all_versions: bool, + pub(crate) show_urls: bool, } impl PythonListSettings { @@ -723,10 +727,14 @@ impl PythonListSettings { all_versions, all_platforms, only_installed, + only_downloads, + show_urls, } = args; let kinds = if only_installed { PythonListKinds::Installed + } else if only_downloads { + PythonListKinds::Downloads } else { PythonListKinds::default() }; @@ -735,6 +743,7 @@ impl PythonListSettings { kinds, all_platforms, all_versions, + show_urls, } } } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index c8eca0f4f078..6c925f47bdac 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4426,6 +4426,10 @@ uv python list [OPTIONS]

When disabled, uv will only use locally cached data and locally available files.

+
--only-downloads

Only show Python downloads, exclude installed distributions.

+ +

By default, available downloads for the current platform are shown.

+
--only-installed

Only show installed Python versions, exclude available downloads.

By default, available downloads for the current platform are shown.

@@ -4458,6 +4462,10 @@ uv python list [OPTIONS]
--quiet, -q

Do not print any output

+
--show-urls

Show the URLs of available Python downloads.

+ +

By default, these display as <download available>.

+
--verbose, -v

Use verbose output.

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)