mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-17 02:52:45 +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()
|
.unwrap()
|
||||||
}
|
}
|
||||||
// Ex) `ruff@0.6.0`
|
// Ex) `ruff@0.6.0`
|
||||||
Target::Version(name, ref version) => {
|
Target::Version(name, ref version) | Target::FromVersion(_, name, ref version) => {
|
||||||
if editable {
|
if editable {
|
||||||
bail!("`--editable` is only supported for local packages");
|
bail!("`--editable` is only supported for local packages");
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ex) `ruff@latest`
|
// Ex) `ruff@latest`
|
||||||
Target::Latest(name) => {
|
Target::Latest(name) | Target::FromLatest(_, name) => {
|
||||||
if editable {
|
if editable {
|
||||||
bail!("`--editable` is only supported for local packages");
|
bail!("`--editable` is only supported for local packages");
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ex) `ruff>=0.6.0`
|
// 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
|
// 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`).
|
// (e.g., `uv install foo==1.0 --from foo`).
|
||||||
let Ok(package) = PackageName::from_str(package) else {
|
let Ok(package) = PackageName::from_str(package) else {
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,49 @@ pub(crate) enum Target<'a> {
|
||||||
Version(&'a str, Version),
|
Version(&'a str, Version),
|
||||||
/// e.g., `ruff@latest`
|
/// e.g., `ruff@latest`
|
||||||
Latest(&'a str),
|
Latest(&'a str),
|
||||||
/// e.g., `--from ruff==0.6.0`
|
/// e.g., `ruff --from ruff>=0.6.0`
|
||||||
UserDefined(&'a str, &'a str),
|
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> {
|
impl<'a> Target<'a> {
|
||||||
/// Parse a target into a command name and a requirement.
|
/// Parse a target into a command name and a requirement.
|
||||||
pub(crate) fn parse(target: &'a str, from: Option<&'a str>) -> Self {
|
pub(crate) fn parse(target: &'a str, from: Option<&'a str>) -> Self {
|
||||||
if let Some(from) = from {
|
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
|
// e.g. `ruff`, no special handling
|
||||||
|
|
@ -72,12 +106,14 @@ impl<'a> Target<'a> {
|
||||||
Self::Unspecified(name) => name,
|
Self::Unspecified(name) => name,
|
||||||
Self::Version(name, _) => name,
|
Self::Version(name, _) => name,
|
||||||
Self::Latest(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`.
|
/// Returns `true` if the target is `latest`.
|
||||||
fn is_latest(&self) -> bool {
|
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,
|
origin: None,
|
||||||
},
|
},
|
||||||
// Ex) `ruff@0.6.0`
|
// Ex) `ruff@0.6.0`
|
||||||
Target::Version(name, version) => Requirement {
|
Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement {
|
||||||
name: PackageName::from_str(name)?,
|
name: PackageName::from_str(name)?,
|
||||||
extras: vec![],
|
extras: vec![],
|
||||||
marker: MarkerTree::default(),
|
marker: MarkerTree::default(),
|
||||||
|
|
@ -365,7 +365,7 @@ async fn get_or_create_environment(
|
||||||
origin: None,
|
origin: None,
|
||||||
},
|
},
|
||||||
// Ex) `ruff@latest`
|
// Ex) `ruff@latest`
|
||||||
Target::Latest(name) => Requirement {
|
Target::Latest(name) | Target::FromLatest(_, name) => Requirement {
|
||||||
name: PackageName::from_str(name)?,
|
name: PackageName::from_str(name)?,
|
||||||
extras: vec![],
|
extras: vec![],
|
||||||
marker: MarkerTree::default(),
|
marker: MarkerTree::default(),
|
||||||
|
|
@ -376,7 +376,7 @@ async fn get_or_create_environment(
|
||||||
origin: None,
|
origin: None,
|
||||||
},
|
},
|
||||||
// Ex) `ruff>=0.6.0`
|
// Ex) `ruff>=0.6.0`
|
||||||
Target::UserDefined(_, from) => resolve_names(
|
Target::From(_, from) => resolve_names(
|
||||||
vec![RequirementsSpecification::parse_package(from)?],
|
vec![RequirementsSpecification::parse_package(from)?],
|
||||||
&interpreter,
|
&interpreter,
|
||||||
settings,
|
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 upgrading an already installed tool via `{package}@{latest}`.
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_at_latest_upgrade() {
|
fn tool_install_at_latest_upgrade() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue