Skip to content

Commit 64b9b1d

Browse files
committed
Respect verbatim executable name in uvx
1 parent 172305a commit 64b9b1d

File tree

2 files changed

+109
-3
lines changed

2 files changed

+109
-3
lines changed

crates/uv/src/commands/tool/run.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,21 @@ async fn get_or_create_environment(
529529
// Ex) `ruff>=0.6.0`
530530
Target::Unspecified(requirement) => {
531531
let spec = RequirementsSpecification::parse_package(requirement)?;
532+
533+
// Extract the verbatim executable name, if possible.
534+
let name = match &spec.requirement {
535+
UnresolvedRequirement::Named(..) => {
536+
// Identify the package name from the PEP 508 specifier.
537+
//
538+
// For example, given `ruff>=0.6.0`, extract `ruff`, to use as the executable name.
539+
let index = requirement
540+
.find(|c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.'))
541+
.unwrap_or(requirement.len());
542+
Some(&requirement[..index])
543+
}
544+
UnresolvedRequirement::Unnamed(..) => None,
545+
};
546+
532547
if let UnresolvedRequirement::Named(requirement) = &spec.requirement {
533548
if requirement.name.as_str() == "python" {
534549
return Err(anyhow::anyhow!(
@@ -539,6 +554,7 @@ async fn get_or_create_environment(
539554
.into());
540555
}
541556
}
557+
542558
let requirement = resolve_names(
543559
vec![spec],
544560
&interpreter,
@@ -556,12 +572,14 @@ async fn get_or_create_environment(
556572
.pop()
557573
.unwrap();
558574

559-
// Use the executable provided by the user, if possible (as in: `uvx --from package executable`).
560-
//
561-
// If no such executable was provided, rely on the package name (as in: `uvx git+https://github.com/pallets/flask`).
575+
// Prefer, in order:
576+
// 1. The verbatim executable provided by the user, independent of the requirement (as in: `uvx --from package executable`).
577+
// 2. The verbatim executable provided by the user as a named requirement (as in: `uvx change_wheel_version`).
578+
// 3. The resolved package name (as in: `uvx git+https://github.com/pallets/flask`).
562579
let executable = request
563580
.executable
564581
.map(ToString::to_string)
582+
.or_else(|| name.map(ToString::to_string))
565583
.unwrap_or_else(|| requirement.name.to_string());
566584

567585
(executable, requirement)

crates/uv/tests/it/tool_run.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,3 +1810,91 @@ fn tool_run_from_at() {
18101810
+ executable-application==0.2.0
18111811
"###);
18121812
}
1813+
1814+
#[test]
1815+
fn tool_run_verbatim_name() {
1816+
let context = TestContext::new("3.12").with_filtered_counts();
1817+
let tool_dir = context.temp_dir.child("tools");
1818+
let bin_dir = context.temp_dir.child("bin");
1819+
1820+
// The normalized package name is `change-wheel-version`, but the executable is `change_wheel_version`.
1821+
uv_snapshot!(context.filters(), context.tool_run()
1822+
.arg("change_wheel_version")
1823+
.arg("--help")
1824+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
1825+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
1826+
success: true
1827+
exit_code: 0
1828+
----- stdout -----
1829+
usage: change_wheel_version [-h] [--local-version LOCAL_VERSION]
1830+
[--version VERSION] [--delete-old-wheel]
1831+
[--allow-same-version]
1832+
wheel
1833+
1834+
positional arguments:
1835+
wheel
1836+
1837+
options:
1838+
-h, --help show this help message and exit
1839+
--local-version LOCAL_VERSION
1840+
--version VERSION
1841+
--delete-old-wheel
1842+
--allow-same-version
1843+
1844+
----- stderr -----
1845+
Resolved [N] packages in [TIME]
1846+
Prepared [N] packages in [TIME]
1847+
Installed [N] packages in [TIME]
1848+
+ change-wheel-version==0.5.0
1849+
+ installer==0.7.0
1850+
+ packaging==24.0
1851+
+ wheel==0.43.0
1852+
"###);
1853+
1854+
uv_snapshot!(context.filters(), context.tool_run()
1855+
.arg("change-wheel-version")
1856+
.arg("--help")
1857+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
1858+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
1859+
success: false
1860+
exit_code: 1
1861+
----- stdout -----
1862+
The executable `change-wheel-version` was not found.
1863+
The following executables are provided by `change-wheel-version`:
1864+
- change_wheel_version
1865+
Consider using `uv tool run --from change-wheel-version <EXECUTABLE_NAME>` instead.
1866+
1867+
----- stderr -----
1868+
Resolved [N] packages in [TIME]
1869+
warning: An executable named `change-wheel-version` is not provided by package `change-wheel-version`.
1870+
"###);
1871+
1872+
uv_snapshot!(context.filters(), context.tool_run()
1873+
.arg("--from")
1874+
.arg("change-wheel-version")
1875+
.arg("change_wheel_version")
1876+
.arg("--help")
1877+
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
1878+
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
1879+
success: true
1880+
exit_code: 0
1881+
----- stdout -----
1882+
usage: change_wheel_version [-h] [--local-version LOCAL_VERSION]
1883+
[--version VERSION] [--delete-old-wheel]
1884+
[--allow-same-version]
1885+
wheel
1886+
1887+
positional arguments:
1888+
wheel
1889+
1890+
options:
1891+
-h, --help show this help message and exit
1892+
--local-version LOCAL_VERSION
1893+
--version VERSION
1894+
--delete-old-wheel
1895+
--allow-same-version
1896+
1897+
----- stderr -----
1898+
Resolved [N] packages in [TIME]
1899+
"###);
1900+
}

0 commit comments

Comments
 (0)