Skip to content

Commit

Permalink
Add uv tool list (#4630)
Browse files Browse the repository at this point in the history
What it says on the tin.

We skip tools with malformed receipts now and warn instead of failing
all tool operations.
  • Loading branch information
zanieb authored Jun 28, 2024
1 parent 948c0f1 commit a444e59
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,8 @@ pub enum ToolCommand {
Run(ToolRunArgs),
/// Install a tool
Install(ToolInstallArgs),
/// List installed tools.
List(ToolListArgs),
}

#[derive(Args)]
Expand Down Expand Up @@ -1969,6 +1971,10 @@ pub struct ToolInstallArgs {
pub python: Option<String>,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct ToolListArgs;

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct ToolchainNamespace {
Expand Down
1 change: 1 addition & 0 deletions crates/uv-tool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 3 additions & 2 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()),
};
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions crates/uv/src/commands/tool/list.rs
Original file line number Diff line number Diff line change
@@ -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<ExitStatus> {
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::<Vec<_>>();
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)
}
1 change: 1 addition & 0 deletions crates/uv/src/commands/tool/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub(crate) mod install;
pub(crate) mod list;
pub(crate) mod run;
10 changes: 10 additions & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,16 @@ async fn run() -> Result<ExitStatus> {
)
.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),
}) => {
Expand Down
18 changes: 17 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<FilesystemOptions>) -> Self {
let ToolListArgs {} = args;

Self {}
}
}

#[derive(Debug, Clone, Default)]
pub(crate) enum ToolchainListKinds {
#[default]
Expand Down
8 changes: 8 additions & 0 deletions crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
85 changes: 85 additions & 0 deletions crates/uv/tests/tool_list.rs
Original file line number Diff line number Diff line change
@@ -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
"###);
}

0 comments on commit a444e59

Please sign in to comment.