mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35: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::borrow::Cow;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::fmt::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
use tokio::process::Command;
|
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 pep440_rs::Version;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_cli::ExternalCommand;
|
use uv_cli::ExternalCommand;
|
||||||
|
@ -20,7 +22,7 @@ use uv_python::{
|
||||||
EnvironmentPreference, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference,
|
EnvironmentPreference, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference,
|
||||||
PythonRequest,
|
PythonRequest,
|
||||||
};
|
};
|
||||||
use uv_tool::InstalledTools;
|
use uv_tool::{entrypoint_paths, InstalledTools};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
use crate::commands::project::environment::CachedEnvironment;
|
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.");
|
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 (target, args) = command.split();
|
||||||
let Some(target) = target else {
|
let Some(target) = target else {
|
||||||
return Err(anyhow::anyhow!("No tool command provided"));
|
return Err(anyhow::anyhow!("No tool command provided"));
|
||||||
|
@ -79,7 +83,6 @@ pub(crate) async fn run(
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// TODO(zanieb): Determine the command via the package entry points
|
// TODO(zanieb): Determine the command via the package entry points
|
||||||
let command = target;
|
let command = target;
|
||||||
|
|
||||||
|
@ -118,9 +121,52 @@ pub(crate) async fn run(
|
||||||
command.to_string_lossy(),
|
command.to_string_lossy(),
|
||||||
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
|
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
|
||||||
);
|
);
|
||||||
let mut handle = process
|
let mut handle = match process.spawn() {
|
||||||
.spawn()
|
Ok(handle) => Ok(handle),
|
||||||
.with_context(|| format!("Failed to spawn: `{}`", command.to_string_lossy()))?;
|
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")?;
|
let status = handle.wait().await.context("Child process disappeared")?;
|
||||||
|
|
||||||
// Exit based on the result of the command
|
// 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.
|
/// 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
|
/// If the target tool is already installed in a compatible environment, returns that
|
||||||
|
|
|
@ -69,7 +69,7 @@ fn tool_run_args() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_run_at_version() {
|
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 tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
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
|
// When `--from` is used, `@` is not treated as a version request
|
||||||
uv_snapshot!(filters, context.tool_run()
|
uv_snapshot!(filters, context.tool_run()
|
||||||
.arg("--from")
|
.arg("--from")
|
||||||
.arg("pytest")
|
.arg("pytest")
|
||||||
.arg("pytest@8.0.0")
|
.arg("pytest@8.0.0")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
The executable pytest@8.0.0 was not found.
|
||||||
|
However, the following executables are available:
|
||||||
|
- py.test
|
||||||
|
- pytest
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
warning: `uv tool run` is experimental and may change without warning.
|
warning: `uv tool run` is experimental and may change without warning.
|
||||||
|
@ -158,8 +162,6 @@ fn tool_run_at_version() {
|
||||||
+ packaging==24.0
|
+ packaging==24.0
|
||||||
+ pluggy==1.4.0
|
+ pluggy==1.4.0
|
||||||
+ pytest==8.1.1
|
+ 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]
|
#[test]
|
||||||
fn tool_run_from_install() {
|
fn tool_run_from_install() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue