mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Add support for specifying name@version
in uv tool run
(#4572)
Instead of requiring `uv tool run --from package==version command` we support `uv tool run command@version` shorthand.
This commit is contained in:
parent
857b3cc777
commit
cb580d1a5d
3 changed files with 144 additions and 8 deletions
|
@ -1861,6 +1861,13 @@ pub enum ToolCommand {
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct ToolRunArgs {
|
pub struct ToolRunArgs {
|
||||||
/// The command to run.
|
/// The command to run.
|
||||||
|
///
|
||||||
|
/// By default, the package to install is assumed to match the command name.
|
||||||
|
///
|
||||||
|
/// The name of the command can include an exact version in the format `<package>@<version>`.
|
||||||
|
///
|
||||||
|
/// If more complex version specification is desired or if the command is provided by a different
|
||||||
|
/// package, use `--from`.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: ExternalCommand,
|
pub command: ExternalCommand,
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use pep440_rs::Version;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
@ -46,16 +50,13 @@ pub(crate) async fn run(
|
||||||
return Err(anyhow::anyhow!("No tool command provided"));
|
return Err(anyhow::anyhow!("No tool command provided"));
|
||||||
};
|
};
|
||||||
|
|
||||||
let from = if let Some(from) = from {
|
let (target, from) = if let Some(from) = from {
|
||||||
from
|
(Cow::Borrowed(target), Cow::Owned(from))
|
||||||
} else {
|
} else {
|
||||||
let Some(target) = target.to_str() else {
|
parse_target(target)?
|
||||||
return Err(anyhow::anyhow!("Tool command could not be parsed as UTF-8 string. Use `--from` to specify the package name."));
|
|
||||||
};
|
|
||||||
target.to_string()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let requirements = [RequirementsSource::from_package(from)]
|
let requirements = [RequirementsSource::from_package(from.to_string())]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(with.into_iter().map(RequirementsSource::from_package))
|
.chain(with.into_iter().map(RequirementsSource::from_package))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -109,7 +110,7 @@ pub(crate) async fn run(
|
||||||
let command = target;
|
let command = target;
|
||||||
|
|
||||||
// Construct the command
|
// Construct the command
|
||||||
let mut process = Command::new(command);
|
let mut process = Command::new(command.as_ref());
|
||||||
process.args(args);
|
process.args(args);
|
||||||
|
|
||||||
// Construct the `PATH` environment variable.
|
// Construct the `PATH` environment variable.
|
||||||
|
@ -167,3 +168,33 @@ pub(crate) async fn run(
|
||||||
Ok(ExitStatus::Failure)
|
Ok(ExitStatus::Failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a target into a command name and a requirement.
|
||||||
|
fn parse_target(target: &OsString) -> Result<(Cow<OsString>, Cow<str>)> {
|
||||||
|
let Some(target_str) = 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."));
|
||||||
|
};
|
||||||
|
|
||||||
|
// e.g. `uv`, no special handling
|
||||||
|
let Some((name, version)) = target_str.split_once('@') else {
|
||||||
|
return Ok((Cow::Borrowed(target), Cow::Borrowed(target_str)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// e.g. `uv@`, warn and treat the whole thing as the command
|
||||||
|
if version.is_empty() {
|
||||||
|
debug!("Ignoring empty version request in command");
|
||||||
|
return Ok((Cow::Borrowed(target), Cow::Borrowed(target_str)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. `uv@0.1.0`, convert to `uv==0.1.0`
|
||||||
|
if let Ok(version) = Version::from_str(version) {
|
||||||
|
return Ok((
|
||||||
|
Cow::Owned(OsString::from(name)),
|
||||||
|
Cow::Owned(format!("{name}=={version}")),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. `uv@invalid`, warn and treat the whole thing as the command
|
||||||
|
debug!("Ignoring invalid version request `{}` in command", version);
|
||||||
|
Ok((Cow::Borrowed(target), Cow::Borrowed(target_str)))
|
||||||
|
}
|
||||||
|
|
|
@ -53,3 +53,101 @@ fn tool_run_args() {
|
||||||
+ pytest==8.1.1
|
+ pytest==8.1.1
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_at_version() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run().arg("pytest@8.0.0").arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
pytest 8.0.0
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool run` is experimental and may change without warning.
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
Prepared 4 packages in [TIME]
|
||||||
|
Installed 4 packages in [TIME]
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pluggy==1.4.0
|
||||||
|
+ pytest==8.0.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Empty versions are just treated as package and command names
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run().arg("pytest@").arg("--version"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool run` is experimental and may change without warning.
|
||||||
|
error: Failed to parse: `pytest@`
|
||||||
|
Caused by: Expected URL
|
||||||
|
pytest@
|
||||||
|
^
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Invalid versions are just treated as package and command names
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run().arg("pytest@invalid").arg("--version"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool run` is experimental and may change without warning.
|
||||||
|
error: Distribution not found at: file://[TEMP_DIR]/invalid
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let filters = context
|
||||||
|
.filters()
|
||||||
|
.into_iter()
|
||||||
|
.chain([(
|
||||||
|
// The error message is different on Windows
|
||||||
|
"Caused by: program not found",
|
||||||
|
"Caused by: No such file or directory (os error 2)",
|
||||||
|
)])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// When `--from` is used, `@` is not treated as a version request
|
||||||
|
uv_snapshot!(filters, context.tool_run().arg("--from").arg("pytest").arg("pytest@8.0.0").arg("--version"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool run` is experimental and may change without warning.
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Installed 4 packages in [TIME]
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pluggy==1.4.0
|
||||||
|
+ pytest==8.1.1
|
||||||
|
error: Failed to spawn: `pytest@8.0.0`
|
||||||
|
Caused by: No such file or directory (os error 2)
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_from_version() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run().arg("--from").arg("pytest==8.0.0").arg("pytest").arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
pytest 8.0.0
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool run` is experimental and may change without warning.
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
Prepared 4 packages in [TIME]
|
||||||
|
Installed 4 packages in [TIME]
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pluggy==1.4.0
|
||||||
|
+ pytest==8.0.0
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue