Skip to content

Commit

Permalink
Allow @ references in uv tool install --from
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 30, 2024
1 parent 97e6861 commit 2b154cb
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 11 deletions.
6 changes: 3 additions & 3 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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");
}
Expand All @@ -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 {
Expand Down
46 changes: 41 additions & 5 deletions crates/uv/src/commands/tool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(_, _))
}
}
6 changes: 3 additions & 3 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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,
Expand Down
90 changes: 90 additions & 0 deletions crates/uv/tests/tool_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2657,6 +2657,96 @@ 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 -----
name: babel
----- 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 = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
]
[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() {
Expand Down

0 comments on commit 2b154cb

Please sign in to comment.