diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b9b638e66..84fc59e9b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4419,6 +4419,10 @@ pub struct ToolListArgs { #[arg(long)] pub show_with: bool, + /// Whether to display the extra requirements installed with each tool. + #[arg(long)] + pub show_extras: bool, + // Hide unused global Python options. #[arg(long, hide = true)] pub python_preference: Option, diff --git a/crates/uv/src/commands/tool/list.rs b/crates/uv/src/commands/tool/list.rs index 4def9cfe4..21b16b020 100644 --- a/crates/uv/src/commands/tool/list.rs +++ b/crates/uv/src/commands/tool/list.rs @@ -13,10 +13,12 @@ use crate::commands::ExitStatus; use crate::printer::Printer; /// List installed tools. +#[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn list( show_paths: bool, show_version_specifiers: bool, show_with: bool, + show_extras: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -80,6 +82,21 @@ pub(crate) async fn list( }) .unwrap_or_default(); + let extra_requirements = show_extras + .then(|| { + tool.requirements() + .iter() + .filter(|req| req.name == name) + .flat_map(|req| req.extras.iter()) // Flatten the extras from all matching requirements + .peekable() + }) + .take_if(|extras| extras.peek().is_some()) + .map(|extras| { + let extras_str = extras.map(ToString::to_string).join(", "); + format!(" [extras: {extras_str}]") + }) + .unwrap_or_default(); + let with_requirements = show_with .then(|| { tool.requirements() @@ -100,14 +117,20 @@ pub(crate) async fn list( writeln!( printer.stdout(), "{} ({})", - format!("{name} v{version}{version_specifier}{with_requirements}").bold(), + format!( + "{name} v{version}{version_specifier}{extra_requirements}{with_requirements}" + ) + .bold(), installed_tools.tool_dir(&name).simplified_display().cyan(), )?; } else { writeln!( printer.stdout(), "{}", - format!("{name} v{version}{version_specifier}{with_requirements}").bold() + format!( + "{name} v{version}{version_specifier}{extra_requirements}{with_requirements}" + ) + .bold() )?; } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b7a43a1ad..a0f543a0c 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1289,6 +1289,7 @@ async fn run(mut cli: Cli) -> Result { args.show_paths, args.show_version_specifiers, args.show_with, + args.show_extras, &cache, printer, ) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1e58536fa..377756dbb 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -782,6 +782,7 @@ pub(crate) struct ToolListSettings { pub(crate) show_paths: bool, pub(crate) show_version_specifiers: bool, pub(crate) show_with: bool, + pub(crate) show_extras: bool, } impl ToolListSettings { @@ -792,6 +793,7 @@ impl ToolListSettings { show_paths, show_version_specifiers, show_with, + show_extras, python_preference: _, no_python_downloads: _, } = args; @@ -800,6 +802,7 @@ impl ToolListSettings { show_paths, show_version_specifiers, show_with, + show_extras, } } } diff --git a/crates/uv/tests/it/tool_list.rs b/crates/uv/tests/it/tool_list.rs index d2c635534..93dd5756e 100644 --- a/crates/uv/tests/it/tool_list.rs +++ b/crates/uv/tests/it/tool_list.rs @@ -452,3 +452,114 @@ fn tool_list_show_with() { ----- stderr ----- "###); } + +#[test] +fn tool_list_show_extras() { + let context = TestContext::new("3.12").with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `black` without extras + context + .tool_install() + .arg("black==24.2.0") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .assert() + .success(); + + // Install `flask` with extras and additional requirements + context + .tool_install() + .arg("flask[async,dotenv]") + .arg("--with") + .arg("requests") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .assert() + .success(); + + // Test with --show-extras only + uv_snapshot!(context.filters(), context.tool_list().arg("--show-extras") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 + - black + - blackd + flask v3.0.2 [extras: async, dotenv] + - flask + + ----- stderr ----- + "###); + + // Test with both --show-extras and --show-with + uv_snapshot!(context.filters(), context.tool_list().arg("--show-extras").arg("--show-with") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 + - black + - blackd + flask v3.0.2 [extras: async, dotenv] [with: requests] + - flask + + ----- stderr ----- + "###); + + // Test with --show-extras and --show-paths + uv_snapshot!(context.filters(), context.tool_list().arg("--show-extras").arg("--show-paths") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 ([TEMP_DIR]/tools/black) + - black ([TEMP_DIR]/bin/black) + - blackd ([TEMP_DIR]/bin/blackd) + flask v3.0.2 [extras: async, dotenv] ([TEMP_DIR]/tools/flask) + - flask ([TEMP_DIR]/bin/flask) + + ----- stderr ----- + "###); + + // Test with --show-extras and --show-version-specifiers + uv_snapshot!(context.filters(), context.tool_list().arg("--show-extras").arg("--show-version-specifiers") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 [required: ==24.2.0] + - black + - blackd + flask v3.0.2 [extras: async, dotenv] + - flask + + ----- stderr ----- + "###); + + // Test with all flags including --show-extras + uv_snapshot!(context.filters(), context.tool_list() + .arg("--show-extras") + .arg("--show-with") + .arg("--show-version-specifiers") + .arg("--show-paths") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black v24.2.0 [required: ==24.2.0] ([TEMP_DIR]/tools/black) + - black ([TEMP_DIR]/bin/black) + - blackd ([TEMP_DIR]/bin/blackd) + flask v3.0.2 [extras: async, dotenv] [with: requests] ([TEMP_DIR]/tools/flask) + - flask ([TEMP_DIR]/bin/flask) + + ----- stderr ----- + "###); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index de9b42238..ad0fce937 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2293,6 +2293,7 @@ uv tool list [OPTIONS]

This setting has no effect when used in the uv pip interface.

May also be set with the UV_PROJECT environment variable.

--quiet, -q

Use quiet output.

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

+
--show-extras

Whether to display the extra requirements installed with each tool

--show-paths

Whether to display the path to each tool environment and installed executable

--show-version-specifiers

Whether to display the version specifier(s) used to install each tool

--show-with

Whether to display the additional requirements installed with each tool