mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-02 18:12:17 +00:00
Require the command in uvx <name>
to be available in the Python environment (#11603)
Closes https://github.com/astral-sh/uv/issues/7804 Includes a few small minor changes to the messaging, but the primary change is that in, e.g., `uvx foo`, if the `foo` package does not provide the `foo` executable we will no longer execute an arbitrary `foo` executable if present on the `PATH`. This prevents confusing and surprising behavior, such as the user reported where they did `uv tool install foobar` (which provides `foo`) then `uvx foo` (which does not provide `foo`) later falls back to the executable provided by `foobar` since it's on the `PATH`. We don't enforce this for `--from`, so things like `uvx --from foo bash -c "..."` are still totally valid. We also still allow `uvx foo` where the `foo` executable is provided by a _dependency_ of `foo` instead of `foo` itself. Most of the diff here is consolidating the logic of the `hint_on_not_found` and `warn_executable_not_provided_by_package ` utilities.
This commit is contained in:
parent
6cc2202799
commit
514a7ea6df
2 changed files with 200 additions and 156 deletions
|
@ -19,6 +19,7 @@ use uv_cli::ExternalCommand;
|
||||||
use uv_client::BaseClientBuilder;
|
use uv_client::BaseClientBuilder;
|
||||||
use uv_configuration::Constraints;
|
use uv_configuration::Constraints;
|
||||||
use uv_configuration::{Concurrency, PreviewMode};
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
|
use uv_distribution_types::InstalledDist;
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource,
|
IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource,
|
||||||
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
|
@ -39,6 +40,7 @@ use uv_shell::runnable::WindowsRunnable;
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
use uv_tool::{entrypoint_paths, InstalledTools};
|
use uv_tool::{entrypoint_paths, InstalledTools};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
use uv_warnings::warn_user_once;
|
||||||
use uv_workspace::WorkspaceCache;
|
use uv_workspace::WorkspaceCache;
|
||||||
|
|
||||||
use crate::commands::pip::loggers::{
|
use crate::commands::pip::loggers::{
|
||||||
|
@ -270,6 +272,7 @@ pub(crate) async fn run(
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
let explicit_from = from.is_some();
|
||||||
let (from, environment) = match result {
|
let (from, environment) = match result {
|
||||||
Ok(resolution) => resolution,
|
Ok(resolution) => resolution,
|
||||||
Err(ProjectError::Operation(err)) => {
|
Err(ProjectError::Operation(err)) => {
|
||||||
|
@ -304,6 +307,38 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
// TODO(zanieb): Determine the executable command via the package entry points
|
// TODO(zanieb): Determine the executable command via the package entry points
|
||||||
let executable = from.executable();
|
let executable = from.executable();
|
||||||
|
let site_packages = SitePackages::from_environment(&environment)?;
|
||||||
|
|
||||||
|
// Check if the provided command is not part of the executables for the `from` package,
|
||||||
|
// and if it's provided by another package in the environment.
|
||||||
|
let provider_hints = match &from {
|
||||||
|
ToolRequirement::Python => None,
|
||||||
|
ToolRequirement::Package { requirement, .. } => Some(ExecutableProviderHints::new(
|
||||||
|
executable,
|
||||||
|
requirement,
|
||||||
|
&site_packages,
|
||||||
|
invocation_source,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref provider_hints) = provider_hints {
|
||||||
|
if provider_hints.not_from_any() {
|
||||||
|
if !explicit_from {
|
||||||
|
// If the user didn't use `--from` and the command isn't in the environment, we're now
|
||||||
|
// just invoking an arbitrary executable on the `PATH` and should exit instead.
|
||||||
|
writeln!(printer.stderr(), "{provider_hints}")?;
|
||||||
|
return Ok(ExitStatus::Failure);
|
||||||
|
}
|
||||||
|
// In the case where `--from` is used, we'll warn on failure if the command is not found
|
||||||
|
// TODO(zanieb): Consider if we should require `--with` instead of `--from` in this case?
|
||||||
|
// It'd be a breaking change but would make `uvx` invocations safer.
|
||||||
|
} else if provider_hints.not_from_expected() {
|
||||||
|
// However, if the user used `--from`, we shouldn't fail because they requested that the
|
||||||
|
// package and executable be different. We'll warn if the executable comes from another
|
||||||
|
// package though, because that could be confusing
|
||||||
|
warn_user_once!("{provider_hints}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Construct the command
|
// Construct the command
|
||||||
let mut process = if cfg!(windows) {
|
let mut process = if cfg!(windows) {
|
||||||
|
@ -327,7 +362,6 @@ pub(crate) async fn run(
|
||||||
|
|
||||||
// Spawn and wait for completion
|
// Spawn and wait for completion
|
||||||
// Standard input, output, and error streams are all inherited
|
// Standard input, output, and error streams are all inherited
|
||||||
// TODO(zanieb): Throw a nicer error message if the command is not found
|
|
||||||
let space = if args.is_empty() { "" } else { " " };
|
let space = if args.is_empty() { "" } else { " " };
|
||||||
debug!(
|
debug!(
|
||||||
"Running `{}{space}{}`",
|
"Running `{}{space}{}`",
|
||||||
|
@ -335,35 +369,17 @@ pub(crate) async fn run(
|
||||||
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
|
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
|
||||||
);
|
);
|
||||||
|
|
||||||
let site_packages = SitePackages::from_environment(&environment)?;
|
|
||||||
|
|
||||||
// We check if the provided command is not part of the executables for the `from` package.
|
|
||||||
// If the command is found in other packages, we warn the user about the correct package to use.
|
|
||||||
match &from {
|
|
||||||
ToolRequirement::Python => {}
|
|
||||||
ToolRequirement::Package {
|
|
||||||
requirement: from, ..
|
|
||||||
} => {
|
|
||||||
warn_executable_not_provided_by_package(
|
|
||||||
executable,
|
|
||||||
&from.name,
|
|
||||||
&site_packages,
|
|
||||||
invocation_source,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let handle = match process.spawn() {
|
let handle = match process.spawn() {
|
||||||
Ok(handle) => Ok(handle),
|
Ok(handle) => Ok(handle),
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
if let Some(exit_status) = hint_on_not_found(
|
if let Some(ref provider_hints) = provider_hints {
|
||||||
executable,
|
if provider_hints.not_from_any() && explicit_from {
|
||||||
&from,
|
// We deferred this warning earlier, because `--from` was used and the command
|
||||||
&site_packages,
|
// could have come from the `PATH`. Display a more helpful message instead of the
|
||||||
invocation_source,
|
// OS error.
|
||||||
printer,
|
writeln!(printer.stderr(), "{provider_hints}")?;
|
||||||
)? {
|
return Ok(ExitStatus::Failure);
|
||||||
return Ok(exit_status);
|
}
|
||||||
}
|
}
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
|
@ -374,67 +390,6 @@ pub(crate) async fn run(
|
||||||
run_to_completion(handle).await
|
run_to_completion(handle).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a hint when a command fails due to a missing executable.
|
|
||||||
///
|
|
||||||
/// Returns an exit status if the caller should exit after hinting.
|
|
||||||
fn hint_on_not_found(
|
|
||||||
executable: &str,
|
|
||||||
from: &ToolRequirement,
|
|
||||||
site_packages: &SitePackages,
|
|
||||||
invocation_source: ToolRunCommand,
|
|
||||||
printer: Printer,
|
|
||||||
) -> anyhow::Result<Option<ExitStatus>> {
|
|
||||||
let from = match from {
|
|
||||||
ToolRequirement::Python => return Ok(None),
|
|
||||||
ToolRequirement::Package {
|
|
||||||
requirement: from, ..
|
|
||||||
} => from,
|
|
||||||
};
|
|
||||||
match get_entrypoints(&from.name, site_packages) {
|
|
||||||
Ok(entrypoints) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stdout(),
|
|
||||||
"The executable `{}` was not found.",
|
|
||||||
executable.cyan(),
|
|
||||||
)?;
|
|
||||||
if entrypoints.is_empty() {
|
|
||||||
warn_user!(
|
|
||||||
"Package `{}` does not provide any executables.",
|
|
||||||
from.name.red()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
warn_user!(
|
|
||||||
"An executable named `{}` is not provided by package `{}`.",
|
|
||||||
executable.cyan(),
|
|
||||||
from.name.red()
|
|
||||||
);
|
|
||||||
writeln!(
|
|
||||||
printer.stdout(),
|
|
||||||
"The following executables are provided by `{}`:",
|
|
||||||
from.name.green()
|
|
||||||
)?;
|
|
||||||
for (name, _) in entrypoints {
|
|
||||||
writeln!(printer.stdout(), "- {}", name.cyan())?;
|
|
||||||
}
|
|
||||||
let suggested_command = format!(
|
|
||||||
"{} --from {} <EXECUTABLE_NAME>",
|
|
||||||
invocation_source, from.name
|
|
||||||
);
|
|
||||||
writeln!(
|
|
||||||
printer.stdout(),
|
|
||||||
"Consider using `{}` instead.",
|
|
||||||
suggested_command.green()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(Some(ExitStatus::Failure))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to get entrypoints for `{from}`: {err}");
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the entry points for the specified package.
|
/// Return the entry points for the specified package.
|
||||||
fn get_entrypoints(
|
fn get_entrypoints(
|
||||||
from: &PackageName,
|
from: &PackageName,
|
||||||
|
@ -517,52 +472,149 @@ async fn show_help(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display a warning if an executable is not provided by package.
|
/// A set of hints about the packages that provide an executable.
|
||||||
///
|
#[derive(Debug)]
|
||||||
/// If found in a dependency of the requested package instead of the requested package itself, we will hint to use that instead.
|
struct ExecutableProviderHints<'a> {
|
||||||
fn warn_executable_not_provided_by_package(
|
/// The requested executable for the command
|
||||||
executable: &str,
|
executable: &'a str,
|
||||||
from_package: &PackageName,
|
/// The package from which the executable is expected to come from
|
||||||
site_packages: &SitePackages,
|
from: &'a Requirement,
|
||||||
|
/// The packages in the [`PythonEnvironment`] the command will run in
|
||||||
|
site_packages: &'a SitePackages,
|
||||||
|
/// The packages with matching executable names
|
||||||
|
packages: Vec<InstalledDist>,
|
||||||
|
/// The source of the invocation, for suggestions to the user
|
||||||
invocation_source: ToolRunCommand,
|
invocation_source: ToolRunCommand,
|
||||||
) {
|
}
|
||||||
let packages = matching_packages(executable, site_packages);
|
|
||||||
if !packages
|
impl<'a> ExecutableProviderHints<'a> {
|
||||||
.iter()
|
fn new(
|
||||||
.any(|package| package.name() == from_package)
|
executable: &'a str,
|
||||||
{
|
from: &'a Requirement,
|
||||||
|
site_packages: &'a SitePackages,
|
||||||
|
invocation_source: ToolRunCommand,
|
||||||
|
) -> Self {
|
||||||
|
let packages = matching_packages(executable, site_packages);
|
||||||
|
ExecutableProviderHints {
|
||||||
|
executable,
|
||||||
|
from,
|
||||||
|
site_packages,
|
||||||
|
packages,
|
||||||
|
invocation_source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the executable is not provided by the expected package.
|
||||||
|
fn not_from_expected(&self) -> bool {
|
||||||
|
!self
|
||||||
|
.packages
|
||||||
|
.iter()
|
||||||
|
.any(|package| package.name() == &self.from.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the executable is not provided by any package.
|
||||||
|
fn not_from_any(&self) -> bool {
|
||||||
|
self.packages.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ExecutableProviderHints<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self {
|
||||||
|
executable,
|
||||||
|
from,
|
||||||
|
site_packages,
|
||||||
|
packages,
|
||||||
|
invocation_source,
|
||||||
|
} = self;
|
||||||
|
|
||||||
match packages.as_slice() {
|
match packages.as_slice() {
|
||||||
[] => {}
|
[] => {
|
||||||
|
let entrypoints = match get_entrypoints(&from.name, site_packages) {
|
||||||
|
Ok(entrypoints) => entrypoints,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to get entrypoints for `{from}`: {err}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if entrypoints.is_empty() {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Package `{}` does not provide any executables.",
|
||||||
|
from.name.red()
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"An executable named `{}` is not provided by package `{}`.",
|
||||||
|
executable.cyan(),
|
||||||
|
from.name.cyan(),
|
||||||
|
)?;
|
||||||
|
writeln!(f, "The following executables are available:")?;
|
||||||
|
for (name, _) in &entrypoints {
|
||||||
|
writeln!(f, "- {}", name.cyan())?;
|
||||||
|
}
|
||||||
|
let name = match entrypoints.as_slice() {
|
||||||
|
[entrypoint] => entrypoint.0.as_str(),
|
||||||
|
_ => "<EXECUTABLE-NAME>",
|
||||||
|
};
|
||||||
|
// If the user didn't use `--from`, suggest it
|
||||||
|
if *executable == from.name.as_str() {
|
||||||
|
let suggested_command =
|
||||||
|
format!("{} --from {} {name}", invocation_source, from.name);
|
||||||
|
writeln!(f, "\nUse `{}` instead.", suggested_command.green().bold())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[package] if package.name() == &from.name => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"An executable named `{}` is provided by package `{}`",
|
||||||
|
executable.cyan(),
|
||||||
|
from.name.cyan(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
[package] => {
|
[package] => {
|
||||||
let suggested_command = format!(
|
let suggested_command = format!(
|
||||||
"{invocation_source} --from {} {}",
|
"{invocation_source} --from {} {}",
|
||||||
package.name(),
|
package.name(),
|
||||||
executable
|
executable
|
||||||
);
|
);
|
||||||
warn_user!(
|
write!(f,
|
||||||
"An executable named `{}` is not provided by package `{}` but is available via the dependency `{}`. Consider using `{}` instead.",
|
"An executable named `{}` is not provided by package `{}` but is available via the dependency `{}`. Consider using `{}` instead.",
|
||||||
executable.cyan(),
|
executable.cyan(),
|
||||||
from_package.cyan(),
|
from.name.cyan(),
|
||||||
package.name().cyan(),
|
package.name().cyan(),
|
||||||
suggested_command.green()
|
suggested_command.green()
|
||||||
);
|
)?;
|
||||||
}
|
}
|
||||||
packages => {
|
packages => {
|
||||||
let suggested_command = format!("{invocation_source} --from PKG {executable}");
|
|
||||||
let provided_by = packages
|
let provided_by = packages
|
||||||
.iter()
|
.iter()
|
||||||
.map(uv_distribution_types::Name::name)
|
.map(uv_distribution_types::Name::name)
|
||||||
.map(|name| format!("- {}", name.cyan()))
|
.map(|name| format!("- {}", name.cyan()))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
warn_user!(
|
if self.not_from_expected() {
|
||||||
"An executable named `{}` is not provided by package `{}` but is available via the following dependencies:\n- {}\nConsider using `{}` instead.",
|
let suggested_command = format!("{invocation_source} --from PKG {executable}");
|
||||||
executable.cyan(),
|
write!(f,
|
||||||
from_package.cyan(),
|
"An executable named `{}` is not provided by package `{}` but is available via the following dependencies:\n- {}\nConsider using `{}` instead.",
|
||||||
provided_by,
|
executable.cyan(),
|
||||||
suggested_command.green(),
|
from.name.cyan(),
|
||||||
);
|
provided_by,
|
||||||
|
suggested_command.green(),
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
write!(f,
|
||||||
|
"An executable named `{}` is provided by package `{}` but is also available via the following dependencies:\n- {}\nUnexpected behavior may occur.",
|
||||||
|
executable.cyan(),
|
||||||
|
from.name.cyan(),
|
||||||
|
provided_by,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,15 +139,10 @@ fn tool_run_at_version() {
|
||||||
.arg("pytest@8.0.0")
|
.arg("pytest@8.0.0")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
The executable `pytest@8.0.0` was not found.
|
|
||||||
The following executables are provided by `pytest`:
|
|
||||||
- py.test
|
|
||||||
- pytest
|
|
||||||
Consider using `uv tool run --from pytest <EXECUTABLE_NAME>` instead.
|
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 4 packages in [TIME]
|
Resolved 4 packages in [TIME]
|
||||||
|
@ -157,8 +152,11 @@ 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
|
||||||
warning: An executable named `pytest@8.0.0` is not provided by package `pytest`.
|
An executable named `pytest@8.0.0` is not provided by package `pytest`.
|
||||||
"###);
|
The following executables are available:
|
||||||
|
- py.test
|
||||||
|
- pytest
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -265,15 +263,10 @@ fn tool_run_suggest_valid_commands() {
|
||||||
.arg("black")
|
.arg("black")
|
||||||
.arg("orange")
|
.arg("orange")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
The executable `orange` was not found.
|
|
||||||
The following executables are provided by `black`:
|
|
||||||
- black
|
|
||||||
- blackd
|
|
||||||
Consider using `uv tool run --from black <EXECUTABLE_NAME>` instead.
|
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 6 packages in [TIME]
|
Resolved 6 packages in [TIME]
|
||||||
|
@ -285,17 +278,19 @@ fn tool_run_suggest_valid_commands() {
|
||||||
+ packaging==24.0
|
+ packaging==24.0
|
||||||
+ pathspec==0.12.1
|
+ pathspec==0.12.1
|
||||||
+ platformdirs==4.2.0
|
+ platformdirs==4.2.0
|
||||||
warning: An executable named `orange` is not provided by package `black`.
|
An executable named `orange` is not provided by package `black`.
|
||||||
"###);
|
The following executables are available:
|
||||||
|
- black
|
||||||
|
- blackd
|
||||||
|
");
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.tool_run()
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
.arg("fastapi-cli")
|
.arg("fastapi-cli")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
The executable `fastapi-cli` was not found.
|
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 3 packages in [TIME]
|
Resolved 3 packages in [TIME]
|
||||||
|
@ -304,8 +299,8 @@ fn tool_run_suggest_valid_commands() {
|
||||||
+ fastapi-cli==0.0.1
|
+ fastapi-cli==0.0.1
|
||||||
+ importlib-metadata==1.7.0
|
+ importlib-metadata==1.7.0
|
||||||
+ zipp==3.18.1
|
+ zipp==3.18.1
|
||||||
warning: Package `fastapi-cli` does not provide any executables.
|
Package `fastapi-cli` does not provide any executables.
|
||||||
"###);
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -327,7 +322,7 @@ fn tool_run_warn_executable_not_in_from() {
|
||||||
.arg("fastapi")
|
.arg("fastapi")
|
||||||
.arg("fastapi")
|
.arg("fastapi")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
@ -371,7 +366,7 @@ fn tool_run_warn_executable_not_in_from() {
|
||||||
+ watchfiles==0.21.0
|
+ watchfiles==0.21.0
|
||||||
+ websockets==12.0
|
+ websockets==12.0
|
||||||
warning: An executable named `fastapi` is not provided by package `fastapi` but is available via the dependency `fastapi-cli`. Consider using `uv tool run --from fastapi-cli fastapi` instead.
|
warning: An executable named `fastapi` is not provided by package `fastapi` but is available via the dependency `fastapi-cli`. Consider using `uv tool run --from fastapi-cli fastapi` instead.
|
||||||
"###);
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1540,11 +1535,10 @@ fn warn_no_executables_found() {
|
||||||
uv_snapshot!(context.filters(), context.tool_run()
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
.arg("requests")
|
.arg("requests")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
The executable `requests` was not found.
|
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 5 packages in [TIME]
|
Resolved 5 packages in [TIME]
|
||||||
|
@ -1555,8 +1549,8 @@ fn warn_no_executables_found() {
|
||||||
+ idna==3.6
|
+ idna==3.6
|
||||||
+ requests==2.31.0
|
+ requests==2.31.0
|
||||||
+ urllib3==2.2.1
|
+ urllib3==2.2.1
|
||||||
warning: Package `requests` does not provide any executables.
|
Package `requests` does not provide any executables.
|
||||||
"###);
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Warn when a user passes `--upgrade` to `uv tool run`.
|
/// Warn when a user passes `--upgrade` to `uv tool run`.
|
||||||
|
@ -2198,19 +2192,19 @@ fn tool_run_verbatim_name() {
|
||||||
.arg("change-wheel-version")
|
.arg("change-wheel-version")
|
||||||
.arg("--help")
|
.arg("--help")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
The executable `change-wheel-version` was not found.
|
|
||||||
The following executables are provided by `change-wheel-version`:
|
|
||||||
- change_wheel_version
|
|
||||||
Consider using `uv tool run --from change-wheel-version <EXECUTABLE_NAME>` instead.
|
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved [N] packages in [TIME]
|
Resolved [N] packages in [TIME]
|
||||||
warning: An executable named `change-wheel-version` is not provided by package `change-wheel-version`.
|
An executable named `change-wheel-version` is not provided by package `change-wheel-version`.
|
||||||
"###);
|
The following executables are available:
|
||||||
|
- change_wheel_version
|
||||||
|
|
||||||
|
Use `uv tool run --from change-wheel-version change_wheel_version` instead.
|
||||||
|
");
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.tool_run()
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
.arg("--from")
|
.arg("--from")
|
||||||
|
@ -2512,16 +2506,14 @@ fn tool_run_windows_runnable_types() -> anyhow::Result<()> {
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
The executable `does_not_exist` was not found.
|
|
||||||
The following executables are provided by `foo`:
|
----- stderr -----
|
||||||
|
An executable named `does_not_exist` is not provided by package `foo`.
|
||||||
|
The following executables are available:
|
||||||
- custom_pydoc.exe
|
- custom_pydoc.exe
|
||||||
- custom_pydoc.bat
|
- custom_pydoc.bat
|
||||||
- custom_pydoc.cmd
|
- custom_pydoc.cmd
|
||||||
- custom_pydoc.ps1
|
- custom_pydoc.ps1
|
||||||
Consider using `uv tool run --from foo <EXECUTABLE_NAME>` instead.
|
|
||||||
|
|
||||||
----- stderr -----
|
|
||||||
warning: An executable named `does_not_exist` is not provided by package `foo`.
|
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Test with explicit .bat extension
|
// Test with explicit .bat extension
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue