mirror of https://github.com/astral-sh/uv
Add `--dry-run` to `uv pip uninstall` (#9557)
## Summary This proposes adding the command line option `uv pip uninstall --dry-run ...`, complementing the existing `uv pip install --dry-run ...` added for #1244 in #1436. This option does not exist in PyPA's `pip uninstall`, if adopted it would be unique to `uv pip`. The code should be considered PoC, it is baby's first Rust. The initial motivation was while investigating https://github.com/moreati/ansible-uv/issues/2 - to allow Ansible module `moreati.uv.pip` to work with`state: absent` in "check_mode" (Ansible's equivalent of a dry run), without requiring `packaging` or `setuptools`. ## Test Plan One new unit test has been added. I pedge to add more if the feature is desired/accepted Example usage ```console ➜ uv git:(pip-uninstall--dry-run) rm -rf .venv ➜ uv git:(pip-uninstall--dry-run) ./target/debug/uv venv Using CPython 3.13.0 Creating virtual environment at: .venv Activate with: source .venv/bin/activate ➜ uv git:(pip-uninstall--dry-run) ./target/debug/uv pip install httpx Resolved 7 packages in 178ms Prepared 5 packages in 60ms Installed 7 packages in 15ms + anyio==4.6.2.post1 + certifi==2024.8.30 + h11==0.14.0 + httpcore==1.0.7 + httpx==0.28.0 + idna==3.10 + sniffio==1.3.1 ➜ uv git:(pip-uninstall--dry-run) ./target/debug/uv pip uninstall --dry-run httpx Would uninstall 1 package - httpx==0.28.0 ➜ uv git:(pip-uninstall--dry-run) ./target/debug/uv pip list Package Version -------- ----------- anyio 4.6.2.post1 certifi 2024.8.30 h11 0.14.0 httpcore 1.0.7 httpx 0.28.0 idna 3.10 sniffio 1.3.1 ``` --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
89a25ba465
commit
8d01f70beb
|
|
@ -1839,6 +1839,10 @@ pub struct PipUninstallArgs {
|
|||
#[arg(long, conflicts_with = "target")]
|
||||
pub prefix: Option<PathBuf>,
|
||||
|
||||
/// Perform a dry run, i.e., don't actually uninstall anything but print the resulting plan.
|
||||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub compat_args: compat::PipGlobalCompatArgs,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use crate::commands::{elapsed, ExitStatus};
|
|||
use crate::printer::Printer;
|
||||
|
||||
/// Uninstall packages from the current environment.
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub(crate) async fn pip_uninstall(
|
||||
sources: &[RequirementsSource],
|
||||
python: Option<String>,
|
||||
|
|
@ -35,6 +36,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();
|
||||
|
|
@ -142,13 +144,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);
|
||||
}
|
||||
|
|
@ -158,13 +162,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);
|
||||
}
|
||||
|
|
@ -177,43 +183,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()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
.dimmed()
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Uninstalled {} {}",
|
||||
format!("{uninstalls} package{s}").bold(),
|
||||
format!("in {}", elapsed(start.elapsed())).dimmed(),
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
|
||||
for distribution in distributions {
|
||||
writeln!(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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’t actually uninstall anything but 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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue