Allow the package spec to be passed positionally in uv tool install (#4564)

Moves `--from` to a hidden argument — we allow it still but we validate
that it is compatible with whatever is passed to `uv tool install
<package>`. The positional package can now be a full specification,
allowing things like `uv tool install black==24.2.0`.
This commit is contained in:
Zanie Blue 2024-06-27 08:35:00 -04:00 committed by GitHub
parent f7fb5a4061
commit 7c3ad62544
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 192 additions and 25 deletions

View file

@ -1908,13 +1908,13 @@ pub struct ToolRunArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct ToolInstallArgs {
/// The command to install.
pub name: String,
/// The package to install commands from.
pub package: String,
/// Use the given package to provide the command.
/// The package to install commands from.
///
/// By default, the package name is assumed to match the command name.
#[arg(long)]
/// This option is provided for parity with `uv tool run`, but is redundant with `package`.
#[arg(long, hide = true)]
pub from: Option<String>,
/// Include the following extra requirements.

View file

@ -30,9 +30,9 @@ use crate::settings::ResolverInstallerSettings;
/// Install a tool.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn install(
name: String,
python: Option<String>,
package: String,
from: Option<String>,
python: Option<String>,
with: Vec<String>,
force: bool,
settings: ResolverInstallerSettings,
@ -47,12 +47,35 @@ pub(crate) async fn install(
if preview.is_disabled() {
warn_user_once!("`uv tool install` is experimental and may change without warning.");
}
let from = from.unwrap_or(name.clone());
let from = if let Some(from) = from {
let from_requirement = Requirement::<VerbatimParsedUrl>::from_str(&from)?;
// Check if the user provided more than just a name positionally or if that name conflicts with `--from`
if from_requirement.name.to_string() != package {
// Determine if its an entirely different package or a conflicting specification
let package_requirement = Requirement::<VerbatimParsedUrl>::from_str(&package)?;
if from_requirement.name == package_requirement.name {
bail!(
"Package requirement `{}` provided with `--from` conflicts with install request `{}`",
from,
package
);
}
bail!(
"Package name `{}` provided with `--from` does not match install request `{}`",
from_requirement.name,
package
);
}
from_requirement
} else {
Requirement::<VerbatimParsedUrl>::from_str(&package)?
};
let name = from.name.to_string();
let installed_tools = InstalledTools::from_settings()?;
// TODO(zanieb): Figure out the interface here, do we infer the name or do we match the `run --from` interface?
let from = Requirement::<VerbatimParsedUrl>::from_str(&from)?;
let existing_tool_receipt = installed_tools.get_tool_receipt(&name)?;
// TODO(zanieb): Automatically replace an existing tool if the request differs
let reinstall_entry_points = if existing_tool_receipt.is_some() {
@ -219,7 +242,7 @@ pub(crate) async fn install(
installed_tools.add_tool_receipt(&name, tool)?;
writeln!(
printer.stdout(),
printer.stderr(),
"Installed: {}",
targets.iter().map(|(name, _, _)| name).join(", ")
)?;

View file

@ -814,9 +814,9 @@ async fn run() -> Result<ExitStatus> {
let cache = cache.init()?.with_refresh(args.refresh);
commands::tool_install(
args.name,
args.python,
args.package,
args.from,
args.python,
args.with,
args.force,
args.settings,

View file

@ -235,7 +235,7 @@ impl ToolRunSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct ToolInstallSettings {
pub(crate) name: String,
pub(crate) package: String,
pub(crate) from: Option<String>,
pub(crate) with: Vec<String>,
pub(crate) python: Option<String>,
@ -249,7 +249,7 @@ impl ToolInstallSettings {
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: ToolInstallArgs, filesystem: Option<FilesystemOptions>) -> Self {
let ToolInstallArgs {
name,
package,
from,
with,
installer,
@ -260,7 +260,7 @@ impl ToolInstallSettings {
} = args;
Self {
name,
package,
from,
with,
python,

View file

@ -27,7 +27,6 @@ fn tool_install() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -40,6 +39,7 @@ fn tool_install() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
tool_dir.child("black").assert(predicate::path::is_dir());
@ -98,7 +98,6 @@ fn tool_install() {
success: true
exit_code: 0
----- stdout -----
Installed: flask
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -112,6 +111,7 @@ fn tool_install() {
+ jinja2==3.1.3
+ markupsafe==2.1.5
+ werkzeug==3.0.1
Installed: flask
"###);
tool_dir.child("flask").assert(predicate::path::is_dir());
@ -158,6 +158,150 @@ fn tool_install() {
});
}
/// Test installing a tool at a version
#[test]
fn tool_install_version() {
let context = TestContext::new("3.12");
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
// Install `black`
uv_snapshot!(context.filters(), context.tool_install()
.arg("black==24.2.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
Resolved 6 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ black==24.2.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
tool_dir.child("black").assert(predicate::path::is_dir());
tool_dir
.child("black")
.child("uv-receipt.toml")
.assert(predicate::path::exists());
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
assert!(executable.exists());
// On Windows, we can't snapshot an executable file.
#[cfg(not(windows))]
insta::with_settings!({
filters => context.filters(),
}, {
// Should run black in the virtual environment
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
#![TEMP_DIR]/tools/black/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from black import patched_main
if __name__ == "__main__":
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
sys.exit(patched_main())
"###);
});
insta::with_settings!({
filters => context.filters(),
}, {
// We should have a tool receipt
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
[tool]
requirements = ["black==24.2.0"]
"###);
});
uv_snapshot!(context.filters(), Command::new("black").arg("--version").env("PATH", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
black, 24.2.0 (compiled: yes)
Python (CPython) 3.12.[X]
----- stderr -----
"###);
}
/// Test installing a tool with `uv tool install --from`
#[test]
fn tool_install_from() {
let context = TestContext::new("3.12");
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
// Install `black` using `--from` to specify the version
uv_snapshot!(context.filters(), context.tool_install()
.arg("black")
.arg("--from")
.arg("black==24.2.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
Resolved 6 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ black==24.2.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
// Attempt to install `black` using `--from` with a different package name
uv_snapshot!(context.filters(), context.tool_install()
.arg("black")
.arg("--from")
.arg("flask==24.2.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
error: Package name `flask` provided with `--from` does not match install request `black`
"###);
// Attempt to install `black` using `--from` with a different version
uv_snapshot!(context.filters(), context.tool_install()
.arg("black==24.2.0")
.arg("--from")
.arg("black==24.3.0")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
error: Package requirement `black==24.3.0` provided with `--from` conflicts with install request `black==24.2.0`
"###);
}
/// Test installing and reinstalling an already installed tool
#[test]
fn tool_install_already_installed() {
@ -173,7 +317,6 @@ fn tool_install_already_installed() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -186,6 +329,7 @@ fn tool_install_already_installed() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
tool_dir.child("black").assert(predicate::path::is_dir());
@ -265,7 +409,6 @@ fn tool_install_already_installed() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -277,6 +420,7 @@ fn tool_install_already_installed() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
// Install `black` again with `--reinstall-package` for `black`
@ -290,7 +434,6 @@ fn tool_install_already_installed() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -299,6 +442,7 @@ fn tool_install_already_installed() {
Installed [N] packages in [TIME]
- black==24.3.0
+ black==24.3.0
Installed: black, blackd
"###);
// Install `black` again with `--reinstall-package` for a dependency
@ -446,7 +590,6 @@ fn tool_install_entry_point_exists() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -458,6 +601,7 @@ fn tool_install_entry_point_exists() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
tool_dir.child("black").assert(predicate::path::is_dir());
@ -526,7 +670,6 @@ fn tool_install_home() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -539,6 +682,7 @@ fn tool_install_home() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
context
@ -562,7 +706,6 @@ fn tool_install_xdg_data_home() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -575,6 +718,7 @@ fn tool_install_xdg_data_home() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
context
@ -598,7 +742,6 @@ fn tool_install_xdg_bin_home() {
success: true
exit_code: 0
----- stdout -----
Installed: black, blackd
----- stderr -----
warning: `uv tool install` is experimental and may change without warning.
@ -611,6 +754,7 @@ fn tool_install_xdg_bin_home() {
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed: black, blackd
"###);
bin_dir