mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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.
This commit is contained in:
parent
948c0f151b
commit
a444e59668
11 changed files with 168 additions and 3 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5031,6 +5031,7 @@ dependencies = [
|
|||
"uv-state",
|
||||
"uv-toolchain",
|
||||
"uv-virtualenv",
|
||||
"uv-warnings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolListArgs;
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct ToolchainNamespace {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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()),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
35
crates/uv/src/commands/tool/list.rs
Normal file
35
crates/uv/src/commands/tool/list.rs
Normal file
|
@ -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,2 +1,3 @@
|
|||
pub(crate) mod install;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod run;
|
||||
|
|
|
@ -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),
|
||||
}) => {
|
||||
|
|
|
@ -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<FilesystemOptions>) -> Self {
|
||||
let ToolListArgs {} = args;
|
||||
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) enum ToolchainListKinds {
|
||||
#[default]
|
||||
|
|
|
@ -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());
|
||||
|
|
85
crates/uv/tests/tool_list.rs
Normal file
85
crates/uv/tests/tool_list.rs
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue