Allow multiple packages for uv tool upgrade/uninstall (#7037)

## Summary

Resolves https://github.com/astral-sh/uv/issues/6571

## Test Plan

`cargo test`
This commit is contained in:
Ahmed Ilyas 2024-09-04 23:18:52 +02:00 committed by GitHub
parent c1effd6b05
commit ff39950545
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 175 additions and 41 deletions

View file

@ -3196,7 +3196,7 @@ pub struct ToolDirArgs {
pub struct ToolUninstallArgs {
/// The name of the tool to uninstall.
#[arg(required = true)]
pub name: Option<PackageName>,
pub name: Option<Vec<PackageName>>,
/// Uninstall all tools.
#[arg(long, conflicts_with("name"))]
@ -3208,7 +3208,7 @@ pub struct ToolUninstallArgs {
pub struct ToolUpgradeArgs {
/// The name of the tool to upgrade.
#[arg(required = true)]
pub name: Option<PackageName>,
pub name: Vec<PackageName>,
/// Upgrade all tools.
#[arg(long, conflicts_with("name"))]

View file

@ -13,13 +13,19 @@ use crate::commands::ExitStatus;
use crate::printer::Printer;
/// Uninstall a tool.
pub(crate) async fn uninstall(name: Option<PackageName>, printer: Printer) -> Result<ExitStatus> {
pub(crate) async fn uninstall(
name: Option<Vec<PackageName>>,
printer: Printer,
) -> Result<ExitStatus> {
let installed_tools = InstalledTools::from_settings()?.init()?;
let _lock = match installed_tools.lock().await {
Ok(lock) => lock,
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
if let Some(name) = name {
bail!("`{name}` is not installed");
if let Some(names) = name {
for name in names {
writeln!(printer.stderr(), "`{name}` is not installed")?;
}
return Ok(ExitStatus::Success);
}
writeln!(printer.stderr(), "Nothing to uninstall")?;
return Ok(ExitStatus::Success);
@ -88,31 +94,35 @@ impl IgnoreCurrentlyBeingDeleted for Result<(), std::io::Error> {
/// Perform the uninstallation.
async fn do_uninstall(
installed_tools: &InstalledTools,
name: Option<PackageName>,
names: Option<Vec<PackageName>>,
printer: Printer,
) -> Result<()> {
let mut dangling = false;
let mut entrypoints = if let Some(name) = name {
let Some(receipt) = installed_tools.get_tool_receipt(&name)? else {
// If the tool is not installed, attempt to remove the environment anyway.
match installed_tools.remove_environment(&name) {
Ok(()) => {
writeln!(
printer.stderr(),
"Removed dangling environment for `{name}`"
)?;
return Ok(());
let mut entrypoints = if let Some(names) = names {
let mut entrypoints = vec![];
for name in names {
let Some(receipt) = installed_tools.get_tool_receipt(&name)? else {
// If the tool is not installed properly, attempt to remove the environment anyway.
match installed_tools.remove_environment(&name) {
Ok(()) => {
writeln!(
printer.stderr(),
"Removed dangling environment for `{name}`"
)?;
return Ok(());
}
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("`{name}` is not installed");
}
Err(err) => {
return Err(err.into());
}
}
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("`{name}` is not installed");
}
Err(err) => {
return Err(err.into());
}
}
};
};
uninstall_tool(&name, &receipt, installed_tools).await?
entrypoints.extend(uninstall_tool(&name, &receipt, installed_tools).await?);
}
entrypoints
} else {
let mut entrypoints = vec![];
for (name, receipt) in installed_tools.tools()? {

View file

@ -21,7 +21,7 @@ use crate::settings::ResolverInstallerSettings;
/// Upgrade a tool.
pub(crate) async fn upgrade(
name: Option<PackageName>,
name: Vec<PackageName>,
connectivity: Connectivity,
args: ResolverInstallerOptions,
filesystem: ResolverInstallerOptions,
@ -34,16 +34,18 @@ pub(crate) async fn upgrade(
let installed_tools = InstalledTools::from_settings()?.init()?;
let _lock = installed_tools.lock().await?;
let names: BTreeSet<PackageName> =
name.map(|name| BTreeSet::from_iter([name]))
.unwrap_or_else(|| {
installed_tools
.tools()
.unwrap_or_default()
.into_iter()
.map(|(name, _)| name)
.collect()
});
let names: BTreeSet<PackageName> = {
if name.is_empty() {
installed_tools
.tools()
.unwrap_or_default()
.into_iter()
.map(|(name, _)| name)
.collect()
} else {
name.into_iter().collect()
}
};
if names.is_empty() {
writeln!(printer.stderr(), "Nothing to upgrade")?;

View file

@ -414,7 +414,7 @@ impl ToolInstallSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct ToolUpgradeSettings {
pub(crate) name: Option<PackageName>,
pub(crate) name: Vec<PackageName>,
pub(crate) args: ResolverInstallerOptions,
pub(crate) filesystem: ResolverInstallerOptions,
}
@ -445,7 +445,7 @@ impl ToolUpgradeSettings {
.unwrap_or_default();
Self {
name: name.filter(|_| !all),
name: if all { vec![] } else { name },
args,
filesystem,
}
@ -473,7 +473,7 @@ impl ToolListSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct ToolUninstallSettings {
pub(crate) name: Option<PackageName>,
pub(crate) name: Option<Vec<PackageName>>,
}
impl ToolUninstallSettings {

View file

@ -68,6 +68,53 @@ fn tool_uninstall() {
"###);
}
#[test]
fn tool_uninstall_multiple_names() {
let context = TestContext::new("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
// Install `black`
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())
.assert()
.success();
context
.tool_install()
.arg("ruff==0.3.4")
.env("UV_TOOL_DIR", tool_dir.as_os_str())
.env("XDG_BIN_HOME", bin_dir.as_os_str())
.assert()
.success();
uv_snapshot!(context.filters(), context.tool_uninstall().arg("black").arg("ruff")
.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 -----
Uninstalled 3 executables: black, blackd, ruff
"###);
// After uninstalling the tool, it shouldn't be listed.
uv_snapshot!(context.filters(), context.tool_list()
.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 -----
No tools installed
"###);
}
#[test]
fn tool_uninstall_not_installed() {
let context = TestContext::new("3.12").with_filtered_exe_suffix();

View file

@ -56,6 +56,81 @@ fn test_tool_upgrade_name() {
"###);
}
#[test]
fn test_tool_upgrade_multiple_names() {
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");
// Install `python-dotenv` from Test PyPI, to get an outdated version.
uv_snapshot!(context.filters(), context.tool_install()
.arg("python-dotenv")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.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]
+ python-dotenv==0.10.2.post2
Installed 1 executable: dotenv
"###);
// Install `babel` from Test PyPI, to get an outdated version.
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.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.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
"###);
// Upgrade `babel` and `python-dotenv` from PyPI.
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("python-dotenv")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.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 -----
Updated babel v2.6.0 -> v2.14.0
- babel==2.6.0
+ babel==2.14.0
- pytz==2018.5
Installed 1 executable: pybabel
Updated python-dotenv v0.10.2.post2 -> v1.0.1
- python-dotenv==0.10.2.post2
+ python-dotenv==1.0.1
Installed 1 executable: dotenv
"###);
}
#[test]
fn test_tool_upgrade_all() {
let context = TestContext::new("3.12")

View file

@ -2838,7 +2838,7 @@ If a tool was installed with specific settings, they will be respected on upgrad
<h3 class="cli-reference">Usage</h3>
```
uv tool upgrade [OPTIONS] <NAME>
uv tool upgrade [OPTIONS] <NAME>...
```
<h3 class="cli-reference">Arguments</h3>
@ -3156,7 +3156,7 @@ Uninstall a tool
<h3 class="cli-reference">Usage</h3>
```
uv tool uninstall [OPTIONS] <NAME>
uv tool uninstall [OPTIONS] <NAME>...
```
<h3 class="cli-reference">Arguments</h3>