Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,11 @@ pub struct PipUninstallArgs {
#[arg(long, conflicts_with = "target")]
pub prefix: Option<PathBuf>,

/// Perform a dry run, i.e., don't actually uninstall anything but resolve the dependencies and
/// print the resulting plan.
#[arg(long)]
pub dry_run: bool,

#[command(flatten)]
pub compat_args: compat::PipGlobalCompatArgs,
}
Expand Down
106 changes: 63 additions & 43 deletions crates/uv/src/commands/pip/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) async fn pip_uninstall(
native_tls: bool,
keyring_provider: KeyringProviderType,
allow_insecure_host: &[TrustedHost],
dry_run: bool,
printer: Printer,
) -> Result<ExitStatus> {
let start = std::time::Instant::now();
Expand Down Expand Up @@ -142,13 +143,15 @@ pub(crate) async fn pip_uninstall(
for package in &names {
let installed = site_packages.get_packages(package);
if installed.is_empty() {
writeln!(
printer.stderr(),
"{}{} Skipping {} as it is not installed",
"warning".yellow().bold(),
":".bold(),
package.as_ref().bold()
)?;
if !dry_run {
writeln!(
printer.stderr(),
"{}{} Skipping {} as it is not installed",
"warning".yellow().bold(),
":".bold(),
package.as_ref().bold()
)?;
}
} else {
distributions.extend(installed);
}
Expand All @@ -158,13 +161,15 @@ pub(crate) async fn pip_uninstall(
for url in &urls {
let installed = site_packages.get_urls(url);
if installed.is_empty() {
writeln!(
printer.stderr(),
"{}{} Skipping {} as it is not installed",
"warning".yellow().bold(),
":".bold(),
url.as_ref().bold()
)?;
if !dry_run {
writeln!(
printer.stderr(),
"{}{} Skipping {} as it is not installed",
"warning".yellow().bold(),
":".bold(),
url.as_ref().bold()
)?;
}
} else {
distributions.extend(installed);
}
Expand All @@ -177,43 +182,58 @@ pub(crate) async fn pip_uninstall(
};

if distributions.is_empty() {
writeln!(
printer.stderr(),
"{}{} No packages to uninstall",
"warning".yellow().bold(),
":".bold(),
)?;
if dry_run {
writeln!(printer.stderr(), "Would make no changes")?;
} else {
writeln!(
printer.stderr(),
"{}{} No packages to uninstall",
"warning".yellow().bold(),
":".bold(),
)?;
}
return Ok(ExitStatus::Success);
}

// Uninstall each package.
for distribution in &distributions {
let summary = uv_installer::uninstall(distribution).await?;
debug!(
"Uninstalled {} ({} file{}, {} director{})",
distribution.name(),
summary.file_count,
if summary.file_count == 1 { "" } else { "s" },
summary.dir_count,
if summary.dir_count == 1 { "y" } else { "ies" },
);
if !dry_run {
for distribution in &distributions {
let summary = uv_installer::uninstall(distribution).await?;
debug!(
"Uninstalled {} ({} file{}, {} director{})",
distribution.name(),
summary.file_count,
if summary.file_count == 1 { "" } else { "s" },
summary.dir_count,
if summary.dir_count == 1 { "y" } else { "ies" },
);
}
}

writeln!(
printer.stderr(),
"{}",
format!(
"Uninstalled {} {}",
let uninstalls = distributions.len();
let s = if uninstalls == 1 { "" } else { "s" };
if dry_run {
writeln!(
printer.stderr(),
"{}",
format!(
"{} package{}",
distributions.len(),
if distributions.len() == 1 { "" } else { "s" }
"Would uninstall {}",
format!("{uninstalls} package{s}").bold(),
)
.bold(),
format!("in {}", elapsed(start.elapsed())).dimmed()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .dimmed() might be redundant, given the one 2 lines down.

)
.dimmed()
)?;
.dimmed()
)?;
} else {
writeln!(
printer.stderr(),
"{}",
format!(
"Uninstalled {} {}",
format!("{uninstalls} package{s}").bold(),
format!("in {}", elapsed(start.elapsed())).dimmed(),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .dimmed() might be redundant, given the one 2 lines down.

)
.dimmed()
)?;
}

for distribution in distributions {
writeln!(
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
globals.native_tls,
args.settings.keyring_provider,
&globals.allow_insecure_host,
args.dry_run,
printer,
)
.await
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,7 @@ impl PipInstallSettings {
pub(crate) struct PipUninstallSettings {
pub(crate) package: Vec<String>,
pub(crate) requirements: Vec<PathBuf>,
pub(crate) dry_run: bool,
pub(crate) settings: PipSettings,
}

Expand All @@ -1777,12 +1778,14 @@ impl PipUninstallSettings {
no_break_system_packages,
target,
prefix,
dry_run,
compat_args: _,
} = args;

Self {
package,
requirements,
dry_run,
settings: PipSettings::combine(
PipOptions {
python: python.and_then(Maybe::into_option),
Expand Down
61 changes: 61 additions & 0 deletions crates/uv/tests/it/pip_uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,64 @@ Version: 0.22.0

Ok(())
}

#[test]
fn dry_run_uninstall_egg_info() -> Result<()> {
let context = TestContext::new("3.12");

let site_packages = ChildPath::new(context.site_packages());

// Manually create a `.egg-info` directory.
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.create_dir_all()?;
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.child("top_level.txt")
.write_str("zstd")?;
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.child("SOURCES.txt")
.write_str("")?;
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.child("PKG-INFO")
.write_str("")?;
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.child("dependency_links.txt")
.write_str("")?;
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.child("entry_points.txt")
.write_str("")?;

// Manually create the package directory.
site_packages.child("zstd").create_dir_all()?;
site_packages
.child("zstd")
.child("__init__.py")
.write_str("")?;

// Run `pip uninstall`.
uv_snapshot!(context.pip_uninstall()
.arg("--dry-run")
.arg("zstandard"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Would uninstall 1 package
- zstandard==0.22.0
"###);

// The `.egg-info` directory should still exist.
assert!(site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.exists());
// The package directory should still exist.
assert!(site_packages.child("zstd").child("__init__.py").exists());

Ok(())
}
2 changes: 2 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -6554,6 +6554,8 @@ uv pip uninstall [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>>

<p>See <code>--project</code> to only change the project root directory.</p>

</dd><dt><code>--dry-run</code></dt><dd><p>Perform a dry run, i.e., don&#8217;t actually uninstall anything but resolve the dependencies and print the resulting plan</p>

</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>

</dd><dt><code>--keyring-provider</code> <i>keyring-provider</i></dt><dd><p>Attempt to use <code>keyring</code> for authentication for remote requirements files.</p>
Expand Down