Allow @ references in uv tool install --from (#6842)

## Summary

Closes https://github.com/astral-sh/uv/issues/6796.
This commit is contained in:
Charlie Marsh 2024-08-30 09:00:17 -04:00 committed by GitHub
parent 3e207da3bc
commit a1805d175e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 135 additions and 11 deletions

View file

@ -117,7 +117,7 @@ pub(crate) async fn install(
.unwrap()
}
// Ex) `ruff@0.6.0`
Target::Version(name, ref version) => {
Target::Version(name, ref version) | Target::FromVersion(_, name, ref version) => {
if editable {
bail!("`--editable` is only supported for local packages");
}
@ -136,7 +136,7 @@ pub(crate) async fn install(
}
}
// Ex) `ruff@latest`
Target::Latest(name) => {
Target::Latest(name) | Target::FromLatest(_, name) => {
if editable {
bail!("`--editable` is only supported for local packages");
}
@ -153,7 +153,7 @@ pub(crate) async fn install(
}
}
// Ex) `ruff>=0.6.0`
Target::UserDefined(package, from) => {
Target::From(package, from) => {
// Parse the positional name. If the user provided more than a package name, it's an error
// (e.g., `uv install foo==1.0 --from foo`).
let Ok(package) = PackageName::from_str(package) else {

View file

@ -22,15 +22,49 @@ pub(crate) enum Target<'a> {
Version(&'a str, Version),
/// e.g., `ruff@latest`
Latest(&'a str),
/// e.g., `--from ruff==0.6.0`
UserDefined(&'a str, &'a str),
/// e.g., `ruff --from ruff>=0.6.0`
From(&'a str, &'a str),
/// e.g., `ruff --from ruff@0.6.0`
FromVersion(&'a str, &'a str, Version),
/// e.g., `ruff --from ruff@latest`
FromLatest(&'a str, &'a str),
}
impl<'a> Target<'a> {
/// Parse a target into a command name and a requirement.
pub(crate) fn parse(target: &'a str, from: Option<&'a str>) -> Self {
if let Some(from) = from {
return Self::UserDefined(target, from);
// e.g. `--from ruff`, no special handling
let Some((name, version)) = from.split_once('@') else {
return Self::From(target, from);
};
// e.g. `--from ruff@`, warn and treat the whole thing as the command
if version.is_empty() {
debug!("Ignoring empty version request in `--from`");
return Self::From(target, from);
}
// e.g., ignore `git+https://github.com/astral-sh/ruff.git@main`
if PackageName::from_str(name).is_err() {
debug!("Ignoring non-package name `{name}` in `--from`");
return Self::From(target, from);
}
match version {
// e.g., `ruff@latest`
"latest" => return Self::FromLatest(target, name),
// e.g., `ruff@0.6.0`
version => {
if let Ok(version) = Version::from_str(version) {
return Self::FromVersion(target, name, version);
}
}
};
// e.g. `--from ruff@invalid`, warn and treat the whole thing as the command
debug!("Ignoring invalid version request `{version}` in `--from`");
return Self::From(target, from);
}
// e.g. `ruff`, no special handling
@ -72,12 +106,14 @@ impl<'a> Target<'a> {
Self::Unspecified(name) => name,
Self::Version(name, _) => name,
Self::Latest(name) => name,
Self::UserDefined(name, _) => name,
Self::FromVersion(name, _, _) => name,
Self::FromLatest(name, _) => name,
Self::From(name, _) => name,
}
}
/// Returns `true` if the target is `latest`.
fn is_latest(&self) -> bool {
matches!(self, Self::Latest(_))
matches!(self, Self::Latest(_) | Self::FromLatest(_, _))
}
}

View file

@ -352,7 +352,7 @@ async fn get_or_create_environment(
origin: None,
},
// Ex) `ruff@0.6.0`
Target::Version(name, version) => Requirement {
Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement {
name: PackageName::from_str(name)?,
extras: vec![],
marker: MarkerTree::default(),
@ -365,7 +365,7 @@ async fn get_or_create_environment(
origin: None,
},
// Ex) `ruff@latest`
Target::Latest(name) => Requirement {
Target::Latest(name) | Target::FromLatest(_, name) => Requirement {
name: PackageName::from_str(name)?,
extras: vec![],
marker: MarkerTree::default(),
@ -376,7 +376,7 @@ async fn get_or_create_environment(
origin: None,
},
// Ex) `ruff>=0.6.0`
Target::UserDefined(_, from) => resolve_names(
Target::From(_, from) => resolve_names(
vec![RequirementsSpecification::parse_package(from)?],
&interpreter,
settings,

View file

@ -2657,6 +2657,94 @@ fn tool_install_at_latest() {
});
}
/// Test installing a tool with `uv tool install {package} --from {package}@latest`.
#[test]
fn tool_install_from_at_latest() {
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");
uv_snapshot!(context.filters(), context.tool_install()
.arg("pybabel")
.arg("--from")
.arg("babel@latest")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.14.0
Installed 1 executable: pybabel
"###);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("babel").join("uv-receipt.toml")).unwrap(), @r###"
[tool]
requirements = [{ name = "babel" }]
entrypoints = [
{ name = "pybabel", install-path = "[TEMP_DIR]/bin/pybabel" },
]
[tool.options]
exclude-newer = "2024-03-25T00:00:00Z"
"###);
});
}
/// Test installing a tool with `uv tool install {package} --from {package}@{version}`.
#[test]
fn tool_install_from_at_version() {
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");
uv_snapshot!(context.filters(), context.tool_install()
.arg("pybabel")
.arg("--from")
.arg("babel@2.13.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.13.0
Installed 1 executable: pybabel
"###);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("babel").join("uv-receipt.toml")).unwrap(), @r###"
[tool]
requirements = [{ name = "babel", specifier = "==2.13.0" }]
entrypoints = [
{ name = "pybabel", install-path = "[TEMP_DIR]/bin/pybabel" },
]
[tool.options]
exclude-newer = "2024-03-25T00:00:00Z"
"###);
});
}
/// Test upgrading an already installed tool via `{package}@{latest}`.
#[test]
fn tool_install_at_latest_upgrade() {