mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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:
parent
f7fb5a4061
commit
7c3ad62544
5 changed files with 192 additions and 25 deletions
|
@ -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.
|
||||
|
|
|
@ -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(", ")
|
||||
)?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue