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.
This commit is contained in:
Charlie Marsh 2025-02-14 16:25:17 -05:00 committed by GitHub
parent 172305abb6
commit 36b4fd2d2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 112 additions and 3 deletions

View file

@ -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)

View file

@ -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 <EXECUTABLE_NAME>` 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]
"###);
}