From 573f5832a3fec247f9c93c809ce2c5e3c034b7a5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 18 Oct 2023 22:30:11 -0400 Subject: [PATCH] Allow uninstall to take multiple packages and files (#125) Moves the command to `puffin pip-uninstall` for now to separate from the managed interface, and redoes the command output. --- crates/install-wheel-rs/src/uninstall.rs | 2 +- crates/puffin-cli/src/commands/mod.rs | 4 +- .../puffin-cli/src/commands/pip_uninstall.rs | 124 ++++++++++++++++++ crates/puffin-cli/src/commands/uninstall.rs | 54 -------- crates/puffin-cli/src/main.rs | 42 ++++-- crates/puffin-cli/src/requirements.rs | 49 +++++++ 6 files changed, 204 insertions(+), 71 deletions(-) create mode 100644 crates/puffin-cli/src/commands/pip_uninstall.rs delete mode 100644 crates/puffin-cli/src/commands/uninstall.rs create mode 100644 crates/puffin-cli/src/requirements.rs diff --git a/crates/install-wheel-rs/src/uninstall.rs b/crates/install-wheel-rs/src/uninstall.rs index 4f716f730..c17bd31d4 100644 --- a/crates/install-wheel-rs/src/uninstall.rs +++ b/crates/install-wheel-rs/src/uninstall.rs @@ -90,7 +90,7 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result { }) } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Uninstall { /// The number of files that were removed during the uninstallation. pub file_count: usize, diff --git a/crates/puffin-cli/src/commands/mod.rs b/crates/puffin-cli/src/commands/mod.rs index 95395a513..0ce96571a 100644 --- a/crates/puffin-cli/src/commands/mod.rs +++ b/crates/puffin-cli/src/commands/mod.rs @@ -6,8 +6,8 @@ pub(crate) use clean::clean; pub(crate) use freeze::freeze; pub(crate) use pip_compile::pip_compile; pub(crate) use pip_sync::{pip_sync, PipSyncFlags}; +pub(crate) use pip_uninstall::pip_uninstall; pub(crate) use remove::remove; -pub(crate) use uninstall::uninstall; pub(crate) use venv::venv; mod add; @@ -15,9 +15,9 @@ mod clean; mod freeze; mod pip_compile; mod pip_sync; +mod pip_uninstall; mod remove; mod reporters; -mod uninstall; mod venv; #[derive(Copy, Clone)] diff --git a/crates/puffin-cli/src/commands/pip_uninstall.rs b/crates/puffin-cli/src/commands/pip_uninstall.rs new file mode 100644 index 000000000..11432c590 --- /dev/null +++ b/crates/puffin-cli/src/commands/pip_uninstall.rs @@ -0,0 +1,124 @@ +use std::fmt::Write; +use std::path::Path; + +use anyhow::Result; +use itertools::Itertools; +use owo_colors::OwoColorize; +use tracing::debug; + +use pep508_rs::Requirement; +use platform_host::Platform; +use puffin_interpreter::PythonExecutable; +use puffin_package::package_name::PackageName; + +use crate::commands::{elapsed, ExitStatus}; +use crate::printer::Printer; +use crate::requirements::RequirementsSource; + +/// Uninstall packages from the current environment. +pub(crate) async fn pip_uninstall( + sources: &[RequirementsSource], + cache: Option<&Path>, + mut printer: Printer, +) -> Result { + let start = std::time::Instant::now(); + + // Detect the current Python interpreter. + let platform = Platform::current()?; + let python = PythonExecutable::from_env(platform, cache)?; + debug!( + "Using Python interpreter: {}", + python.executable().display() + ); + + // Read all requirements from the provided sources. + let requirements = sources + .iter() + .map(RequirementsSource::requirements) + .flatten_ok() + .collect::>>()?; + + // Index the current `site-packages` directory. + let site_packages = puffin_interpreter::SitePackages::from_executable(&python).await?; + + // Sort and deduplicate the requirements. + let packages = { + let mut packages = requirements + .into_iter() + .map(|requirement| PackageName::normalize(requirement.name)) + .collect::>(); + packages.sort_unstable(); + packages.dedup(); + packages + }; + + // Map to the local distributions. + let dist_infos = packages + .iter() + .filter_map(|package| { + if let Some(dist_info) = site_packages.get(package) { + Some(dist_info) + } else { + let _ = writeln!( + printer, + "{}{} Skipping {} as it is not installed.", + "warning".yellow().bold(), + ":".bold(), + package.bold() + ); + None + } + }) + .collect::>(); + + if dist_infos.is_empty() { + writeln!( + printer, + "{}{} No packages to uninstall.", + "warning".yellow().bold(), + ":".bold(), + )?; + return Ok(ExitStatus::Success); + } + + // Uninstall each package. + for dist_info in &dist_infos { + let summary = puffin_installer::uninstall(dist_info).await?; + debug!( + "Uninstalled {} ({} file{}, {} director{})", + dist_info.name(), + summary.file_count, + if summary.file_count == 1 { "" } else { "s" }, + summary.dir_count, + if summary.dir_count == 1 { "y" } else { "ies" }, + ); + } + + writeln!( + printer, + "{}", + format!( + "Uninstalled {} in {}", + format!( + "{} package{}", + dist_infos.len(), + if dist_infos.len() == 1 { "" } else { "s" } + ) + .bold(), + elapsed(start.elapsed()) + ) + .dimmed() + )?; + + for dist_info in dist_infos { + writeln!( + printer, + " {} {}{}", + "-".red(), + dist_info.name().as_ref().white().bold(), + format!("@{}", dist_info.version()).dimmed() + )?; + } + + Ok(ExitStatus::Success) +} diff --git a/crates/puffin-cli/src/commands/uninstall.rs b/crates/puffin-cli/src/commands/uninstall.rs deleted file mode 100644 index bcbdc3b5b..000000000 --- a/crates/puffin-cli/src/commands/uninstall.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::fmt::Write; -use std::path::Path; - -use anyhow::{anyhow, Result}; -use tracing::debug; - -use platform_host::Platform; -use puffin_interpreter::PythonExecutable; -use puffin_package::package_name::PackageName; - -use crate::commands::ExitStatus; -use crate::printer::Printer; - -/// Uninstall a package from the current environment. -pub(crate) async fn uninstall( - name: &str, - cache: Option<&Path>, - mut printer: Printer, -) -> Result { - // Detect the current Python interpreter. - let platform = Platform::current()?; - let python = PythonExecutable::from_env(platform, cache)?; - debug!( - "Using Python interpreter: {}", - python.executable().display() - ); - - // Index the current `site-packages` directory. - let site_packages = puffin_interpreter::SitePackages::from_executable(&python).await?; - - // Locate the package in the environment. - let Some(dist_info) = site_packages.get(&PackageName::normalize(name)) else { - return Err(anyhow!("Package not installed: {}", name)); - }; - - // Uninstall the package from the environment. - let uninstall = puffin_installer::uninstall(dist_info).await?; - - // Print a summary of the uninstallation. - match (uninstall.file_count, uninstall.dir_count) { - (0, 0) => writeln!(printer, "No files found")?, - (1, 0) => writeln!(printer, "Removed 1 file")?, - (0, 1) => writeln!(printer, "Removed 1 directory")?, - (1, 1) => writeln!(printer, "Removed 1 file and 1 directory")?, - (file_count, 0) => writeln!(printer, "Removed {file_count} files")?, - (0, dir_count) => writeln!(printer, "Removed {dir_count} directories")?, - (file_count, dir_count) => writeln!( - printer, - "Removed {file_count} files and {dir_count} directories" - )?, - } - - Ok(ExitStatus::Success) -} diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index c71c84931..cd9639249 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -6,10 +6,12 @@ use directories::ProjectDirs; use owo_colors::OwoColorize; use crate::commands::ExitStatus; +use crate::requirements::RequirementsSource; mod commands; mod logging; mod printer; +mod requirements; #[derive(Parser)] #[command(author, version, about)] @@ -41,8 +43,8 @@ enum Commands { Clean, /// Enumerate the installed packages in the current environment. Freeze, - /// Uninstall a package. - Uninstall(UninstallArgs), + /// Uninstall packages from the current environment. + PipUninstall(PipUninstallArgs), /// Create a virtual environment. Venv(VenvArgs), /// Add a dependency to the workspace. @@ -71,9 +73,15 @@ struct PipSyncArgs { } #[derive(Args)] -struct UninstallArgs { - /// The name of the package to uninstall. - name: String, +#[command(group = clap::ArgGroup::new("sources").required(true))] +struct PipUninstallArgs { + /// Uninstall all listed packages. + #[clap(group = "sources")] + package: Vec, + + /// Uninstall all packages listed in the given requirements files. + #[clap(short, long, group = "sources")] + requirement: Vec, } #[derive(Args)] @@ -119,7 +127,7 @@ async fn main() -> ExitCode { let dirs = ProjectDirs::from("", "", "puffin"); - let result = match &cli.command { + let result = match cli.command { Commands::PipCompile(args) => { commands::pip_compile( &args.src, @@ -146,11 +154,15 @@ async fn main() -> ExitCode { ) .await } - Commands::Clean => { - commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await - } - Commands::Freeze => { - commands::freeze( + Commands::PipUninstall(args) => { + let sources = args + .package + .into_iter() + .map(RequirementsSource::from) + .chain(args.requirement.into_iter().map(RequirementsSource::from)) + .collect::>(); + commands::pip_uninstall( + &sources, dirs.as_ref() .map(ProjectDirs::cache_dir) .filter(|_| !cli.no_cache), @@ -158,9 +170,11 @@ async fn main() -> ExitCode { ) .await } - Commands::Uninstall(args) => { - commands::uninstall( - &args.name, + Commands::Clean => { + commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await + } + Commands::Freeze => { + commands::freeze( dirs.as_ref() .map(ProjectDirs::cache_dir) .filter(|_| !cli.no_cache), diff --git a/crates/puffin-cli/src/requirements.rs b/crates/puffin-cli/src/requirements.rs new file mode 100644 index 000000000..689e8e754 --- /dev/null +++ b/crates/puffin-cli/src/requirements.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Result; +use itertools::Either; + +use pep508_rs::Requirement; +use puffin_package::requirements_txt::RequirementsTxt; + +#[derive(Debug)] +pub(crate) enum RequirementsSource { + /// A dependency was provided on the command line (e.g., `pip install flask`). + Name(String), + /// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`). + Path(PathBuf), +} + +impl From for RequirementsSource { + fn from(name: String) -> Self { + Self::Name(name) + } +} + +impl From for RequirementsSource { + fn from(path: PathBuf) -> Self { + Self::Path(path) + } +} + +impl RequirementsSource { + /// Return an iterator over the requirements in this source. + pub(crate) fn requirements(&self) -> Result> { + match self { + Self::Name(name) => { + let requirement = Requirement::from_str(name)?; + Ok(Either::Left(std::iter::once(requirement))) + } + Self::Path(path) => { + let requirements_txt = RequirementsTxt::parse(path, std::env::current_dir()?)?; + Ok(Either::Right( + requirements_txt + .requirements + .into_iter() + .map(|entry| entry.requirement), + )) + } + } + } +}