mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-13 20:12:03 +00:00
Return error when running uvx with a .py script (#11623)
If we see `uvx script.py`, we exit early, giving a hint to use `uv run script.py` if the script exists. If it does not exist, we suggest running `uv run` with a normalized package name. This PR includes a snapshot test for each of these scenarios. An alternative approach would be to wait until we encounter an error, and then add the hint. But if there happens to be a malicious package called `script-py`, this would be run unintentionally (a point raised by @zanieb). Closes #10784 --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
3b83b48fd2
commit
c072c9adca
2 changed files with 161 additions and 2 deletions
|
@ -1,5 +1,6 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ use uv_configuration::{Concurrency, PreviewMode};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
Name, NameRequirementSpecification, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
Name, NameRequirementSpecification, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
|
@ -92,6 +94,12 @@ pub(crate) async fn run(
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> anyhow::Result<ExitStatus> {
|
) -> anyhow::Result<ExitStatus> {
|
||||||
|
/// Whether or not a path looks like a Python script based on the file extension.
|
||||||
|
fn has_python_script_ext(path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw"))
|
||||||
|
}
|
||||||
|
|
||||||
let Some(command) = command else {
|
let Some(command) = command else {
|
||||||
// When a command isn't provided, we'll show a brief help including available tools
|
// When a command isn't provided, we'll show a brief help including available tools
|
||||||
show_help(invocation_source, &cache, printer).await?;
|
show_help(invocation_source, &cache, printer).await?;
|
||||||
|
@ -105,9 +113,46 @@ pub(crate) async fn run(
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(target) = target.to_str() else {
|
let Some(target) = target.to_str() else {
|
||||||
return Err(anyhow::anyhow!("Tool command could not be parsed as UTF-8 string. Use `--from` to specify the package name."));
|
return Err(anyhow::anyhow!("Tool command could not be parsed as UTF-8 string. Use `--from` to specify the package name"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(ref from) = from {
|
||||||
|
if has_python_script_ext(Path::new(from)) {
|
||||||
|
let package_name = PackageName::from_str(from)?;
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"It looks you provided a Python script to `--from`, which is not supported\n\n{}{} If you meant to run a command from the `{}` package, use the normalized package name instead to disambiguate, e.g., `{}`",
|
||||||
|
"hint".bold().cyan(),
|
||||||
|
":".bold(),
|
||||||
|
package_name.cyan(),
|
||||||
|
format!("{} --from {} {}", invocation_source, package_name.cyan(), target),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let target_path = Path::new(target);
|
||||||
|
if has_python_script_ext(target_path) {
|
||||||
|
return if target_path.try_exists()? {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"It looks you tried to run a Python script at `{}`, which is not supported by `{}`\n\n{}{} Use `{}` instead",
|
||||||
|
target_path.user_display(),
|
||||||
|
invocation_source,
|
||||||
|
"hint".bold().cyan(),
|
||||||
|
":".bold(),
|
||||||
|
format!("uv run {}", target_path.user_display().cyan()),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let package_name = PackageName::from_str(target)?;
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"It looks you provided a Python script to run, which is not supported supported by `{}`\n\n{}{} We did not find a script at the requested path. If you meant to run a command from the `{}` package, pass the normalized package name to `--from` to disambiguate, e.g., `{}`",
|
||||||
|
invocation_source,
|
||||||
|
"hint".bold().cyan(),
|
||||||
|
":".bold(),
|
||||||
|
package_name.cyan(),
|
||||||
|
format!("{} --from {} {}", invocation_source, package_name, target),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let request = ToolRequest::parse(target, from.as_deref());
|
let request = ToolRequest::parse(target, from.as_deref());
|
||||||
|
|
||||||
// If the user passed, e.g., `ruff@latest`, refresh the cache.
|
// If the user passed, e.g., `ruff@latest`, refresh the cache.
|
||||||
|
|
|
@ -1760,7 +1760,7 @@ fn tool_run_python_at_version() {
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Invalid version request: 3.300
|
error: Invalid version request: 3.300
|
||||||
"###);
|
"###);
|
||||||
|
@ -1962,3 +1962,117 @@ fn tool_run_verbatim_name() {
|
||||||
Resolved [N] packages in [TIME]
|
Resolved [N] packages in [TIME]
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_with_existing_py_script() -> anyhow::Result<()> {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
context.temp_dir.child("script.py").touch()?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run().arg("script.py"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: It looks you tried to run a Python script at `script.py`, which is not supported by `uv tool run`
|
||||||
|
|
||||||
|
hint: Use `uv run script.py` instead
|
||||||
|
");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_with_existing_pyw_script() -> anyhow::Result<()> {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
context.temp_dir.child("script.pyw").touch()?;
|
||||||
|
|
||||||
|
// We treat arguments before the command as uv arguments
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("script.pyw"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: It looks you tried to run a Python script at `script.pyw`, which is not supported by `uv tool run`
|
||||||
|
|
||||||
|
hint: Use `uv run script.pyw` instead
|
||||||
|
");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_with_nonexistent_py_script() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
|
||||||
|
// We treat arguments before the command as uv arguments
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("script.py"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: It looks you provided a Python script to run, which is not supported supported by `uv tool run`
|
||||||
|
|
||||||
|
hint: We did not find a script at the requested path. If you meant to run a command from the `script-py` package, pass the normalized package name to `--from` to disambiguate, e.g., `uv tool run --from script-py script.py`
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_with_nonexistent_pyw_script() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
|
||||||
|
// We treat arguments before the command as uv arguments
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("script.pyw"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: It looks you provided a Python script to run, which is not supported supported by `uv tool run`
|
||||||
|
|
||||||
|
hint: We did not find a script at the requested path. If you meant to run a command from the `script-pyw` package, pass the normalized package name to `--from` to disambiguate, e.g., `uv tool run --from script-pyw script.pyw`
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_with_from_script() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
|
||||||
|
// We treat arguments before the command as uv arguments
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("--from")
|
||||||
|
.arg("script.py")
|
||||||
|
.arg("ruff"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: It looks you provided a Python script to `--from`, which is not supported
|
||||||
|
|
||||||
|
hint: If you meant to run a command from the `script-py` package, use the normalized package name instead to disambiguate, e.g., `uv tool run --from script-py ruff`
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_with_script_and_from_script() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_counts();
|
||||||
|
|
||||||
|
// We treat arguments before the command as uv arguments
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("--from")
|
||||||
|
.arg("script.py")
|
||||||
|
.arg("other-script.py"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: It looks you provided a Python script to `--from`, which is not supported
|
||||||
|
|
||||||
|
hint: If you meant to run a command from the `script-py` package, use the normalized package name instead to disambiguate, e.g., `uv tool run --from script-py other-script.py`
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue