From b22f43ffacac8c838e6c5b25acf27c08b24b5d06 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 28 Jun 2024 12:11:20 -0500 Subject: [PATCH] Add `uv tool list` --- Cargo.lock | 1 + crates/uv-cli/src/lib.rs | 6 ++ crates/uv-tool/Cargo.toml | 1 + crates/uv-tool/src/lib.rs | 5 +- crates/uv/src/commands/mod.rs | 1 + crates/uv/src/commands/tool/list.rs | 35 ++++++++++++ crates/uv/src/commands/tool/mod.rs | 1 + crates/uv/src/main.rs | 10 ++++ crates/uv/src/settings.rs | 18 +++++- crates/uv/tests/common/mod.rs | 8 +++ crates/uv/tests/tool_list.rs | 85 +++++++++++++++++++++++++++++ 11 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 crates/uv/src/commands/tool/list.rs create mode 100644 crates/uv/tests/tool_list.rs diff --git a/Cargo.lock b/Cargo.lock index dfba16a08a07..d316f5dbc216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5031,6 +5031,7 @@ dependencies = [ "uv-state", "uv-toolchain", "uv-virtualenv", + "uv-warnings", ] [[package]] diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9ff0d8ab7ccd..1db5a21fe634 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1873,6 +1873,8 @@ pub enum ToolCommand { Run(ToolRunArgs), /// Install a tool Install(ToolInstallArgs), + /// List installed tools. + List(ToolListArgs), } #[derive(Args)] @@ -1969,6 +1971,10 @@ pub struct ToolInstallArgs { pub python: Option, } +#[derive(Args)] +#[allow(clippy::struct_excessive_bools)] +pub struct ToolListArgs; + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct ToolchainNamespace { diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index 4eae2f0b716d..b2a3e17f1302 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -21,6 +21,7 @@ uv-virtualenv = { workspace = true } uv-toolchain = { workspace = true } install-wheel-rs = { workspace = true } pep440_rs = { workspace = true } +uv-warnings = { workspace = true } uv-cache = { workspace = true } thiserror = { workspace = true } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index ee8f6b96d486..5c8764ea3409 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -11,6 +11,7 @@ use tracing::debug; use uv_cache::Cache; use uv_fs::{LockedFile, Simplified}; use uv_toolchain::{Interpreter, PythonEnvironment}; +use uv_warnings::warn_user_once; pub use receipt::ToolReceipt; pub use tool::Tool; @@ -80,9 +81,9 @@ impl InstalledTools { let path = directory.join("uv-receipt.toml"); let contents = match fs_err::read_to_string(&path) { Ok(contents) => contents, - // TODO(zanieb): Consider warning on malformed tools instead Err(err) if err.kind() == io::ErrorKind::NotFound => { - return Err(Error::MissingToolReceipt(name.clone(), path.clone())) + warn_user_once!("Ignoring malformed tool `{name}`: missing receipt"); + continue; } Err(err) => return Err(err.into()), }; diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 99d4ea001ac1..7af049be3cec 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -25,6 +25,7 @@ pub(crate) use project::sync::sync; #[cfg(feature = "self-update")] pub(crate) use self_update::self_update; pub(crate) use tool::install::install as tool_install; +pub(crate) use tool::list::list as tool_list; pub(crate) use tool::run::run as tool_run; pub(crate) use toolchain::find::find as toolchain_find; pub(crate) use toolchain::install::install as toolchain_install; diff --git a/crates/uv/src/commands/tool/list.rs b/crates/uv/src/commands/tool/list.rs new file mode 100644 index 000000000000..6f6a3a9e40fc --- /dev/null +++ b/crates/uv/src/commands/tool/list.rs @@ -0,0 +1,35 @@ +use std::fmt::Write; + +use anyhow::Result; + +use uv_configuration::PreviewMode; +use uv_tool::InstalledTools; +use uv_warnings::warn_user_once; + +use crate::commands::ExitStatus; +use crate::printer::Printer; + +/// List installed tools. +#[allow(clippy::too_many_arguments)] +pub(crate) async fn list(preview: PreviewMode, printer: Printer) -> Result { + if preview.is_disabled() { + warn_user_once!("`uv tool list` is experimental and may change without warning."); + } + + let installed_tools = InstalledTools::from_settings()?; + + let mut tools = installed_tools.tools()?.into_iter().collect::>(); + tools.sort_by_key(|(name, _)| name.clone()); + + if tools.is_empty() { + writeln!(printer.stderr(), "No tools installed")?; + return Ok(ExitStatus::Success); + } + + // TODO(zanieb): Track and display additional metadata, like entry points + for (name, _tool) in tools { + writeln!(printer.stdout(), "{name}")?; + } + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/tool/mod.rs b/crates/uv/src/commands/tool/mod.rs index 2f9db0be0078..0fb53090a759 100644 --- a/crates/uv/src/commands/tool/mod.rs +++ b/crates/uv/src/commands/tool/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod install; +pub(crate) mod list; pub(crate) mod run; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index e52ca69cb271..1fd0c7a4e30a 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -830,6 +830,16 @@ async fn run() -> Result { ) .await } + + Commands::Tool(ToolNamespace { + command: ToolCommand::List(args), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::ToolListSettings::resolve(args, filesystem); + show_settings!(args); + + commands::tool_list(globals.preview, printer).await + } Commands::Toolchain(ToolchainNamespace { command: ToolchainCommand::List(args), }) => { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 106aeae513de..a1c356b92f76 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -14,7 +14,8 @@ use uv_cli::{ AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs, - ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs, + ToolListArgs, ToolRunArgs, ToolchainFindArgs, ToolchainInstallArgs, ToolchainListArgs, + VenvArgs, }; use uv_client::Connectivity; use uv_configuration::{ @@ -275,6 +276,21 @@ impl ToolInstallSettings { } } +/// The resolved settings to use for a `tool list` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct ToolListSettings; + +impl ToolListSettings { + /// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve(args: ToolListArgs, _filesystem: Option) -> Self { + let ToolListArgs {} = args; + + Self {} + } +} + #[derive(Debug, Clone, Default)] pub(crate) enum ToolchainListKinds { #[default] diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index bf97cc87e632..05973f9656a3 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -412,6 +412,14 @@ impl TestContext { command } + /// Create a `uv tool list` command with options shared across scenarios. + pub fn tool_list(&self) -> std::process::Command { + let mut command = std::process::Command::new(get_bin()); + command.arg("tool").arg("list"); + self.add_shared_args(&mut command); + command + } + /// Create a `uv add` command for the given requirements. pub fn add(&self, reqs: &[&str]) -> Command { let mut command = Command::new(get_bin()); diff --git a/crates/uv/tests/tool_list.rs b/crates/uv/tests/tool_list.rs new file mode 100644 index 000000000000..a54f15c9cd12 --- /dev/null +++ b/crates/uv/tests/tool_list.rs @@ -0,0 +1,85 @@ +#![cfg(all(feature = "python", feature = "pypi"))] + +use assert_cmd::assert::OutputAssertExt; +use assert_fs::fixture::PathChild; +use common::{uv_snapshot, TestContext}; + +mod common; + +#[test] +fn tool_list() { + let context = TestContext::new("3.12"); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `black` + context + .tool_install() + .arg("black==24.2.0") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .assert() + .success(); + + uv_snapshot!(context.filters(), context.tool_list() + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + black + + ----- stderr ----- + warning: `uv tool list` is experimental and may change without warning. + "###); +} + +#[test] +fn tool_list_empty() { + let context = TestContext::new("3.12"); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + uv_snapshot!(context.filters(), context.tool_list() + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv tool list` is experimental and may change without warning. + No tools installed + "###); +} + +#[test] +fn tool_list_missing_receipt() { + let context = TestContext::new("3.12"); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `black` + context + .tool_install() + .arg("black==24.2.0") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .assert() + .success(); + + fs_err::remove_file(tool_dir.join("black").join("uv-receipt.toml")).unwrap(); + + uv_snapshot!(context.filters(), context.tool_list() + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv tool list` is experimental and may change without warning. + warning: Ignoring malformed tool `black`: missing receipt + No tools installed + "###); +}