Skip to content

Commit

Permalink
Add uv tool list
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Jun 28, 2024
1 parent 948c0f1 commit bca8d50
Show file tree
Hide file tree
Showing 11 changed files with 172 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
9 changes: 7 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,13 @@ 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 `{}`: missing receipt at {}",
name,
path.user_display()
);
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 at [TEMP_DIR]/tools/black/uv-receipt.toml
No tools installed
"###);
}

0 comments on commit bca8d50

Please sign in to comment.