Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uv tool list #4630

Merged
merged 1 commit into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}")?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include the version, like pip list?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems nice!

I'll probably do in a follow-up to separate it from all this boilerplate.

}

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