add --show-extras to uv tool list to list extra requirements installed with tools (#13783)

## Summary

Implemented as suggested in #13761 

eg.

```
$ uv tool install 'harlequin[postgres]'
$ uv tool list --show-extras
harlequin v2.1.2 [extras: postgres]
- harlequin
```

## Test Plan

Added a new test with the argument along with the others from the `uv
tool list` cli.
This commit is contained in:
Tobias Gårdhus 2025-06-02 16:59:40 +02:00 committed by GitHub
parent 5400434957
commit 459c902425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 145 additions and 2 deletions

View file

@ -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<PythonPreference>,

View file

@ -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<ExitStatus> {
@ -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()
)?;
}

View file

@ -1289,6 +1289,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.show_paths,
args.show_version_specifiers,
args.show_with,
args.show_extras,
&cache,
printer,
)

View file

@ -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,
}
}
}

View file

@ -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 -----
"###);
}

View file

@ -2293,6 +2293,7 @@ uv tool list [OPTIONS]
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
<p>May also be set with the <code>UV_PROJECT</code> environment variable.</p></dd><dt id="uv-tool-list--quiet"><a href="#uv-tool-list--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Use quiet output.</p>
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
</dd><dt id="uv-tool-list--show-extras"><a href="#uv-tool-list--show-extras"><code>--show-extras</code></a></dt><dd><p>Whether to display the extra requirements installed with each tool</p>
</dd><dt id="uv-tool-list--show-paths"><a href="#uv-tool-list--show-paths"><code>--show-paths</code></a></dt><dd><p>Whether to display the path to each tool environment and installed executable</p>
</dd><dt id="uv-tool-list--show-version-specifiers"><a href="#uv-tool-list--show-version-specifiers"><code>--show-version-specifiers</code></a></dt><dd><p>Whether to display the version specifier(s) used to install each tool</p>
</dd><dt id="uv-tool-list--show-with"><a href="#uv-tool-list--show-with"><code>--show-with</code></a></dt><dd><p>Whether to display the additional requirements installed with each tool</p>