From 36b4fd2d2dbc421d1b7d55c776db9802a98d1387 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 14 Feb 2025 16:25:17 -0500 Subject: [PATCH] Respect verbatim executable name in uvx (#11524) ## Summary If the user provides a PEP 508 requirement (e.g., `uvx change_wheel_version`), then we should us that verbatim for the executable, rather than normalizing the package name. Closes https://github.com/astral-sh/uv/issues/11521. --- crates/uv/src/commands/tool/run.rs | 25 ++++++++- crates/uv/tests/it/tool_run.rs | 90 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index d3decef3b..2bd056e1c 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -529,6 +529,22 @@ async fn get_or_create_environment( // Ex) `ruff>=0.6.0` Target::Unspecified(requirement) => { let spec = RequirementsSpecification::parse_package(requirement)?; + + // Extract the verbatim executable name, if possible. + let name = match &spec.requirement { + UnresolvedRequirement::Named(..) => { + // Identify the package name from the PEP 508 specifier. + // + // For example, given `ruff>=0.6.0`, extract `ruff`, to use as the executable name. + let content = requirement.trim(); + let index = content + .find(|c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.')) + .unwrap_or(content.len()); + Some(&content[..index]) + } + UnresolvedRequirement::Unnamed(..) => None, + }; + if let UnresolvedRequirement::Named(requirement) = &spec.requirement { if requirement.name.as_str() == "python" { return Err(anyhow::anyhow!( @@ -539,6 +555,7 @@ async fn get_or_create_environment( .into()); } } + let requirement = resolve_names( vec![spec], &interpreter, @@ -556,12 +573,14 @@ async fn get_or_create_environment( .pop() .unwrap(); - // Use the executable provided by the user, if possible (as in: `uvx --from package executable`). - // - // If no such executable was provided, rely on the package name (as in: `uvx git+https://github.com/pallets/flask`). + // Prefer, in order: + // 1. The verbatim executable provided by the user, independent of the requirement (as in: `uvx --from package executable`). + // 2. The verbatim executable provided by the user as a named requirement (as in: `uvx change_wheel_version`). + // 3. The resolved package name (as in: `uvx git+https://github.com/pallets/flask`). let executable = request .executable .map(ToString::to_string) + .or_else(|| name.map(ToString::to_string)) .unwrap_or_else(|| requirement.name.to_string()); (executable, requirement) diff --git a/crates/uv/tests/it/tool_run.rs b/crates/uv/tests/it/tool_run.rs index cf72570a1..ede1f1871 100644 --- a/crates/uv/tests/it/tool_run.rs +++ b/crates/uv/tests/it/tool_run.rs @@ -1810,3 +1810,93 @@ fn tool_run_from_at() { + executable-application==0.2.0 "###); } + +#[test] +fn tool_run_verbatim_name() { + let context = TestContext::new("3.12") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // The normalized package name is `change-wheel-version`, but the executable is `change_wheel_version`. + uv_snapshot!(context.filters(), context.tool_run() + .arg("change_wheel_version") + .arg("--help") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + usage: change_wheel_version [-h] [--local-version LOCAL_VERSION] + [--version VERSION] [--delete-old-wheel] + [--allow-same-version] + wheel + + positional arguments: + wheel + + options: + -h, --help show this help message and exit + --local-version LOCAL_VERSION + --version VERSION + --delete-old-wheel + --allow-same-version + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + change-wheel-version==0.5.0 + + installer==0.7.0 + + packaging==24.0 + + wheel==0.43.0 + "###); + + uv_snapshot!(context.filters(), context.tool_run() + .arg("change-wheel-version") + .arg("--help") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: false + exit_code: 1 + ----- 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 ` instead. + + ----- stderr ----- + Resolved [N] packages in [TIME] + warning: An executable named `change-wheel-version` is not provided by package `change-wheel-version`. + "###); + + uv_snapshot!(context.filters(), context.tool_run() + .arg("--from") + .arg("change-wheel-version") + .arg("change_wheel_version") + .arg("--help") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + usage: change_wheel_version [-h] [--local-version LOCAL_VERSION] + [--version VERSION] [--delete-old-wheel] + [--allow-same-version] + wheel + + positional arguments: + wheel + + options: + -h, --help show this help message and exit + --local-version LOCAL_VERSION + --version VERSION + --delete-old-wheel + --allow-same-version + + ----- stderr ----- + Resolved [N] packages in [TIME] + "###); +}