Skip to content

Commit

Permalink
Allow multiple packages for uv tool upgrade/uninstall (#7037)
Browse files Browse the repository at this point in the history
## Summary

Resolves #6571

## Test Plan

`cargo test`
  • Loading branch information
blueraft authored Sep 4, 2024
1 parent c1effd6 commit ff39950
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 41 deletions.
4 changes: 2 additions & 2 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand All @@ -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"))]
Expand Down
56 changes: 33 additions & 23 deletions crates/uv/src/commands/tool/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(());
}
Err(uv_tool::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("`{name}` is not installed");
}
Err(err) => {
return Err(err.into());
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());
}
}
}
};
};

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()? {
Expand Down
24 changes: 13 additions & 11 deletions crates/uv/src/commands/tool/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")?;
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -445,7 +445,7 @@ impl ToolUpgradeSettings {
.unwrap_or_default();

Self {
name: name.filter(|_| !all),
name: if all { vec![] } else { name },
args,
filesystem,
}
Expand Down Expand Up @@ -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 {
Expand Down
47 changes: 47 additions & 0 deletions crates/uv/tests/tool_uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
75 changes: 75 additions & 0 deletions crates/uv/tests/tool_upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit ff39950

Please sign in to comment.