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

uvx/uv tool run: Add context message before listing available tools when no arguments are provided #7641

Merged
merged 4 commits into from
Sep 26, 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
83 changes: 78 additions & 5 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ use crate::commands::pip::operations;
use crate::commands::project::{resolve_names, EnvironmentSpecification, ProjectError};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::tool::Target;
use crate::commands::{
project::environment::CachedEnvironment, tool::common::matching_packages, tool_list,
};
use crate::commands::{project::environment::CachedEnvironment, tool::common::matching_packages};
use crate::commands::{ExitStatus, SharedState};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;
Expand Down Expand Up @@ -79,9 +77,13 @@ pub(crate) async fn run(
cache: Cache,
printer: Printer,
) -> anyhow::Result<ExitStatus> {
// treat empty command as `uv tool list`
// Treat empty command similar to `uv tool list`, list available tools.
let Some(command) = command else {
return tool_list(false, false, &cache, printer).await;
match list_available_tools(invocation_source, &cache, printer).await {
// It is a failure because user misses a required tool name.
Ok(()) => return Ok(ExitStatus::Error),
Err(err) => return Err(err),
};
};

let (target, args) = command.split();
Expand Down Expand Up @@ -262,6 +264,77 @@ fn get_entrypoints(
)?)
}

/// Display a list of tools that provide the executable.
///
/// If there is no package providing the executable, we will display a message to how to install a package.
async fn list_available_tools(
invocation_source: ToolRunCommand,
cache: &Cache,
printer: Printer,
) -> anyhow::Result<()> {
writeln!(
printer.stdout(),
"Provide a command to invoke with `{invocation_source} <command>` \
or `{invocation_source} --from <package> <command>`.\n"
)?;

let installed_tools = InstalledTools::from_settings()?;
let no_tools_installed_msg =
"No tools installed. See `uv tool install --help` for more information.";
let _lock = match installed_tools.lock().await {
Ok(lock) => lock,
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
writeln!(printer.stdout(), "{no_tools_installed_msg}")?;
return Ok(());
}
Err(err) => return Err(err.into()),
};

let mut tools = installed_tools.tools()?.into_iter().collect::<Vec<_>>();
tools.sort_by_key(|(name, _)| name.clone());

if tools.is_empty() {
writeln!(printer.stdout(), "{no_tools_installed_msg}")?;
return Ok(());
}

let mut buf = String::new();
for (name, tool) in tools {
// Skip invalid tools.
let Ok(tool) = tool else {
continue;
};

// Output tool name and version.
let Ok(version) = installed_tools.version(&name, cache) else {
continue;
};
writeln!(buf, "{}", format!("{name} v{version}").bold())?;

// Output tool entrypoints.
for entrypoint in tool.entrypoints() {
writeln!(buf, "- {}", entrypoint.name)?;
}
}

// Installed tools were malformed or failed fetching versions.
if buf.is_empty() {
writeln!(printer.stderr(), "{no_tools_installed_msg}")?;
return Ok(());
}

writeln!(
printer.stdout(),
"The following tools are already installed:\n"
)?;
writeln!(printer.stdout(), "{buf}")?;
writeln!(
printer.stdout(),
"See `{invocation_source} --help` for more information."
)?;
Ok(())
}

/// Display a warning if an executable is not provided by package.
///
/// If found in a dependency of the requested package instead of the requested package itself, we will hint to use that instead.
Expand Down
23 changes: 15 additions & 8 deletions crates/uv/tests/tool_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,12 +750,14 @@ fn tool_run_list_installed() {
uv_snapshot!(context.filters(), context.tool_run()
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
success: false
exit_code: 2
----- stdout -----
Provide a command to invoke with `uv tool run <command>` or `uv tool run --from <package> <command>`.

No tools installed. See `uv tool install --help` for more information.

----- stderr -----
No tools installed
"###);

// Install `black`.
Expand All @@ -771,13 +773,19 @@ fn tool_run_list_installed() {
uv_snapshot!(context.filters(), context.tool_run()
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
success: false
exit_code: 2
----- stdout -----
Provide a command to invoke with `uv tool run <command>` or `uv tool run --from <package> <command>`.

The following tools are already installed:

black v24.2.0
- black
- blackd

See `uv tool run --help` for more information.

----- stderr -----
"###);
}
Expand Down Expand Up @@ -891,9 +899,8 @@ fn tool_run_with_editable() -> anyhow::Result<()> {
"###);

// Requesting an editable requirement should install it in a layer, even if it satisfied
uv_snapshot!(context.filters(), context.tool_run().arg("--with-editable").arg("./src/anyio_local").arg("flask").arg("--version").env("UV_TOOL_DIR", tool_dir.as_os_str()).env("XDG_BIN_HOME", bin_dir.as_os_str())

, @r###"
uv_snapshot!(context.filters(), context.tool_run().arg("--with-editable").arg("./src/anyio_local").arg("flask").arg("--version").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 -----
Expand Down
Loading