mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +00:00
Allow @ references in uv tool install --from (#6842)
## Summary Closes https://github.com/astral-sh/uv/issues/6796.
This commit is contained in:
parent
3e207da3bc
commit
a1805d175e
4 changed files with 135 additions and 11 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(_, _))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue