mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
uv tool run
suggest valid commands when command is not found (#4997)
## Summary Resolves #4979. ## Test Plan `cargo test` <img width="619" alt="Screenshot 2024-07-11 at 22 45 36" src="https://github.com/user-attachments/assets/62526010-9123-43f5-9f8d-1f9e89f6be59"> <img width="636" alt="Screenshot 2024-07-11 at 22 45 23" src="https://github.com/user-attachments/assets/a348cd73-f891-40b1-8934-afbd1aa19326">
This commit is contained in:
parent
6949796110
commit
23c6cd774b
2 changed files with 136 additions and 18 deletions
|
@ -1,14 +1,16 @@
|
|||
use std::borrow::Cow;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use tokio::process::Command;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use distribution_types::UnresolvedRequirementSpecification;
|
||||
use distribution_types::{Name, UnresolvedRequirementSpecification};
|
||||
use pep440_rs::Version;
|
||||
use uv_cache::Cache;
|
||||
use uv_cli::ExternalCommand;
|
||||
|
@ -20,7 +22,7 @@ use uv_python::{
|
|||
EnvironmentPreference, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference,
|
||||
PythonRequest,
|
||||
};
|
||||
use uv_tool::InstalledTools;
|
||||
use uv_tool::{entrypoint_paths, InstalledTools};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::commands::project::environment::CachedEnvironment;
|
||||
|
@ -51,6 +53,8 @@ pub(crate) async fn run(
|
|||
warn_user_once!("`uv tool run` is experimental and may change without warning.");
|
||||
}
|
||||
|
||||
let has_from = from.is_some();
|
||||
|
||||
let (target, args) = command.split();
|
||||
let Some(target) = target else {
|
||||
return Err(anyhow::anyhow!("No tool command provided"));
|
||||
|
@ -79,7 +83,6 @@ pub(crate) async fn run(
|
|||
printer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO(zanieb): Determine the command via the package entry points
|
||||
let command = target;
|
||||
|
||||
|
@ -118,9 +121,52 @@ pub(crate) async fn run(
|
|||
command.to_string_lossy(),
|
||||
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
|
||||
);
|
||||
let mut handle = process
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to spawn: `{}`", command.to_string_lossy()))?;
|
||||
let mut handle = match process.spawn() {
|
||||
Ok(handle) => Ok(handle),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
match get_entrypoints(&from, &environment) {
|
||||
Ok(entrypoints) => {
|
||||
if entrypoints.is_empty() {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"The executable {} was not found.",
|
||||
command.to_string_lossy().red(),
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"The executable {} was not found.",
|
||||
command.to_string_lossy().red()
|
||||
)?;
|
||||
if has_from {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, the following executables are available:",
|
||||
)?;
|
||||
} else {
|
||||
let command = format!("uv tool run --from {from} <EXECUTABLE>");
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, the following executables are available via {}:",
|
||||
command.green(),
|
||||
)?;
|
||||
}
|
||||
for (name, _) in entrypoints {
|
||||
writeln!(printer.stdout(), "- {}", name.cyan())?;
|
||||
}
|
||||
}
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get entrypoints for `{from}`: {err}");
|
||||
}
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
.with_context(|| format!("Failed to spawn: `{}`", command.to_string_lossy()))?;
|
||||
|
||||
let status = handle.wait().await.context("Child process disappeared")?;
|
||||
|
||||
// Exit based on the result of the command
|
||||
|
@ -132,6 +178,23 @@ pub(crate) async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the entry points for the specified package.
|
||||
fn get_entrypoints(from: &str, environment: &PythonEnvironment) -> Result<Vec<(String, PathBuf)>> {
|
||||
let site_packages = SitePackages::from_environment(environment)?;
|
||||
let package = PackageName::from_str(from)?;
|
||||
|
||||
let installed = site_packages.get_packages(&package);
|
||||
let Some(installed_dist) = installed.first().copied() else {
|
||||
bail!("Expected at least one requirement")
|
||||
};
|
||||
|
||||
Ok(entrypoint_paths(
|
||||
environment,
|
||||
installed_dist.name(),
|
||||
installed_dist.version(),
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Get or create a [`PythonEnvironment`] in which to run the specified tools.
|
||||
///
|
||||
/// If the target tool is already installed in a compatible environment, returns that
|
||||
|
|
|
@ -69,7 +69,7 @@ fn tool_run_args() {
|
|||
|
||||
#[test]
|
||||
fn tool_run_at_version() {
|
||||
let context = TestContext::new("3.12");
|
||||
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
|
@ -139,15 +139,19 @@ fn tool_run_at_version() {
|
|||
|
||||
// When `--from` is used, `@` is not treated as a version request
|
||||
uv_snapshot!(filters, context.tool_run()
|
||||
.arg("--from")
|
||||
.arg("pytest")
|
||||
.arg("pytest@8.0.0")
|
||||
.arg("--version")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
.arg("--from")
|
||||
.arg("pytest")
|
||||
.arg("pytest@8.0.0")
|
||||
.arg("--version")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
The executable pytest@8.0.0 was not found.
|
||||
However, the following executables are available:
|
||||
- py.test
|
||||
- pytest
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool run` is experimental and may change without warning.
|
||||
|
@ -158,8 +162,6 @@ fn tool_run_at_version() {
|
|||
+ packaging==24.0
|
||||
+ pluggy==1.4.0
|
||||
+ pytest==8.1.1
|
||||
error: Failed to spawn: `pytest@8.0.0`
|
||||
Caused by: No such file or directory (os error 2)
|
||||
"###);
|
||||
}
|
||||
|
||||
|
@ -193,6 +195,59 @@ fn tool_run_from_version() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_suggest_valid_commands() {
|
||||
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("--from")
|
||||
.arg("black")
|
||||
.arg("orange")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
The executable orange was not found.
|
||||
However, the following executables are available:
|
||||
- black
|
||||
- blackd
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool run` is experimental and may change without warning.
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 6 packages in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
+ black==24.3.0
|
||||
+ click==8.1.7
|
||||
+ mypy-extensions==1.0.0
|
||||
+ packaging==24.0
|
||||
+ pathspec==0.12.1
|
||||
+ platformdirs==4.2.0
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("fastapi-cli")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
The executable fastapi-cli was not found.
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool run` is experimental and may change without warning.
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ fastapi-cli==0.0.1
|
||||
+ importlib-metadata==1.7.0
|
||||
+ zipp==3.18.1
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_from_install() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue