Skip to content

Commit a444e59

Browse files
authored
Add uv tool list (#4630)
What it says on the tin. We skip tools with malformed receipts now and warn instead of failing all tool operations.
1 parent 948c0f1 commit a444e59

File tree

11 files changed

+168
-3
lines changed

11 files changed

+168
-3
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-cli/src/lib.rs

+6
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,8 @@ pub enum ToolCommand {
18731873
Run(ToolRunArgs),
18741874
/// Install a tool
18751875
Install(ToolInstallArgs),
1876+
/// List installed tools.
1877+
List(ToolListArgs),
18761878
}
18771879

18781880
#[derive(Args)]
@@ -1969,6 +1971,10 @@ pub struct ToolInstallArgs {
19691971
pub python: Option<String>,
19701972
}
19711973

1974+
#[derive(Args)]
1975+
#[allow(clippy::struct_excessive_bools)]
1976+
pub struct ToolListArgs;
1977+
19721978
#[derive(Args)]
19731979
#[allow(clippy::struct_excessive_bools)]
19741980
pub struct ToolchainNamespace {

crates/uv-tool/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ uv-virtualenv = { workspace = true }
2121
uv-toolchain = { workspace = true }
2222
install-wheel-rs = { workspace = true }
2323
pep440_rs = { workspace = true }
24+
uv-warnings = { workspace = true }
2425
uv-cache = { workspace = true }
2526

2627
thiserror = { workspace = true }

crates/uv-tool/src/lib.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tracing::debug;
1111
use uv_cache::Cache;
1212
use uv_fs::{LockedFile, Simplified};
1313
use uv_toolchain::{Interpreter, PythonEnvironment};
14+
use uv_warnings::warn_user_once;
1415

1516
pub use receipt::ToolReceipt;
1617
pub use tool::Tool;
@@ -80,9 +81,9 @@ impl InstalledTools {
8081
let path = directory.join("uv-receipt.toml");
8182
let contents = match fs_err::read_to_string(&path) {
8283
Ok(contents) => contents,
83-
// TODO(zanieb): Consider warning on malformed tools instead
8484
Err(err) if err.kind() == io::ErrorKind::NotFound => {
85-
return Err(Error::MissingToolReceipt(name.clone(), path.clone()))
85+
warn_user_once!("Ignoring malformed tool `{name}`: missing receipt");
86+
continue;
8687
}
8788
Err(err) => return Err(err.into()),
8889
};

crates/uv/src/commands/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub(crate) use project::sync::sync;
2525
#[cfg(feature = "self-update")]
2626
pub(crate) use self_update::self_update;
2727
pub(crate) use tool::install::install as tool_install;
28+
pub(crate) use tool::list::list as tool_list;
2829
pub(crate) use tool::run::run as tool_run;
2930
pub(crate) use toolchain::find::find as toolchain_find;
3031
pub(crate) use toolchain::install::install as toolchain_install;

crates/uv/src/commands/tool/list.rs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use std::fmt::Write;
2+
3+
use anyhow::Result;
4+
5+
use uv_configuration::PreviewMode;
6+
use uv_tool::InstalledTools;
7+
use uv_warnings::warn_user_once;
8+
9+
use crate::commands::ExitStatus;
10+
use crate::printer::Printer;
11+
12+
/// List installed tools.
13+
#[allow(clippy::too_many_arguments)]
14+
pub(crate) async fn list(preview: PreviewMode, printer: Printer) -> Result<ExitStatus> {
15+
if preview.is_disabled() {
16+
warn_user_once!("`uv tool list` is experimental and may change without warning.");
17+
}
18+
19+
let installed_tools = InstalledTools::from_settings()?;
20+
21+
let mut tools = installed_tools.tools()?.into_iter().collect::<Vec<_>>();
22+
tools.sort_by_key(|(name, _)| name.clone());
23+
24+
if tools.is_empty() {
25+
writeln!(printer.stderr(), "No tools installed")?;
26+
return Ok(ExitStatus::Success);
27+
}
28+
29+
// TODO(zanieb): Track and display additional metadata, like entry points
30+
for (name, _tool) in tools {
31+
writeln!(printer.stdout(), "{name}")?;
32+
}
33+
34+
Ok(ExitStatus::Success)
35+
}

crates/uv/src/commands/tool/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub(crate) mod install;
2+
pub(crate) mod list;
23
pub(crate) mod run;

crates/uv/src/main.rs

+10
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,16 @@ async fn run() -> Result<ExitStatus> {
830830
)
831831
.await
832832
}
833+
834+
Commands::Tool(ToolNamespace {
835+
command: ToolCommand::List(args),
836+
}) => {
837+
// Resolve the settings from the command-line arguments and workspace configuration.
838+
let args = settings::ToolListSettings::resolve(args, filesystem);
839+
show_settings!(args);
840+
841+
commands::tool_list(globals.preview, printer).await
842+
}
833843
Commands::Toolchain(ToolchainNamespace {
834844
command: ToolchainCommand::List(args),
835845
}) => {

crates/uv/src/settings.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use uv_cli::{
1414
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
1515
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
1616
PipSyncArgs, PipTreeArgs, PipUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs,
17-
ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs,
17+
ToolListArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs,
18+
VenvArgs,
1819
};
1920
use uv_client::Connectivity;
2021
use uv_configuration::{
@@ -275,6 +276,21 @@ impl ToolInstallSettings {
275276
}
276277
}
277278

279+
/// The resolved settings to use for a `tool list` invocation.
280+
#[allow(clippy::struct_excessive_bools)]
281+
#[derive(Debug, Clone)]
282+
pub(crate) struct ToolListSettings;
283+
284+
impl ToolListSettings {
285+
/// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration.
286+
#[allow(clippy::needless_pass_by_value)]
287+
pub(crate) fn resolve(args: ToolListArgs, _filesystem: Option<FilesystemOptions>) -> Self {
288+
let ToolListArgs {} = args;
289+
290+
Self {}
291+
}
292+
}
293+
278294
#[derive(Debug, Clone, Default)]
279295
pub(crate) enum ToolchainListKinds {
280296
#[default]

crates/uv/tests/common/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,14 @@ impl TestContext {
412412
command
413413
}
414414

415+
/// Create a `uv tool list` command with options shared across scenarios.
416+
pub fn tool_list(&self) -> std::process::Command {
417+
let mut command = std::process::Command::new(get_bin());
418+
command.arg("tool").arg("list");
419+
self.add_shared_args(&mut command);
420+
command
421+
}
422+
415423
/// Create a `uv add` command for the given requirements.
416424
pub fn add(&self, reqs: &[&str]) -> Command {
417425
let mut command = Command::new(get_bin());

crates/uv/tests/tool_list.rs

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#![cfg(all(feature = "python", feature = "pypi"))]
2+
3+
use assert_cmd::assert::OutputAssertExt;
4+
use assert_fs::fixture::PathChild;
5+
use common::{uv_snapshot, TestContext};
6+
7+
mod common;
8+
9+
#[test]
10+
fn tool_list() {
11+
let context = TestContext::new("3.12");
12+
let tool_dir = context.temp_dir.child("tools");
13+
let bin_dir = context.temp_dir.child("bin");
14+
15+
// Install `black`
16+
context
17+
.tool_install()
18+
.arg("black==24.2.0")
19+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
20+
.env("XDG_BIN_HOME", bin_dir.as_os_str())
21+
.assert()
22+
.success();
23+
24+
uv_snapshot!(context.filters(), context.tool_list()
25+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
26+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
27+
success: true
28+
exit_code: 0
29+
----- stdout -----
30+
black
31+
32+
----- stderr -----
33+
warning: `uv tool list` is experimental and may change without warning.
34+
"###);
35+
}
36+
37+
#[test]
38+
fn tool_list_empty() {
39+
let context = TestContext::new("3.12");
40+
let tool_dir = context.temp_dir.child("tools");
41+
let bin_dir = context.temp_dir.child("bin");
42+
43+
uv_snapshot!(context.filters(), context.tool_list()
44+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
45+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
46+
success: true
47+
exit_code: 0
48+
----- stdout -----
49+
50+
----- stderr -----
51+
warning: `uv tool list` is experimental and may change without warning.
52+
No tools installed
53+
"###);
54+
}
55+
56+
#[test]
57+
fn tool_list_missing_receipt() {
58+
let context = TestContext::new("3.12");
59+
let tool_dir = context.temp_dir.child("tools");
60+
let bin_dir = context.temp_dir.child("bin");
61+
62+
// Install `black`
63+
context
64+
.tool_install()
65+
.arg("black==24.2.0")
66+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
67+
.env("XDG_BIN_HOME", bin_dir.as_os_str())
68+
.assert()
69+
.success();
70+
71+
fs_err::remove_file(tool_dir.join("black").join("uv-receipt.toml")).unwrap();
72+
73+
uv_snapshot!(context.filters(), context.tool_list()
74+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
75+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
76+
success: true
77+
exit_code: 0
78+
----- stdout -----
79+
80+
----- stderr -----
81+
warning: `uv tool list` is experimental and may change without warning.
82+
warning: Ignoring malformed tool `black`: missing receipt
83+
No tools installed
84+
"###);
85+
}

0 commit comments

Comments
 (0)