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.
This commit is contained in:
Charlie Marsh 2023-10-18 22:30:11 -04:00 committed by GitHub
parent 4b91ae4769
commit 573f5832a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 71 deletions

View file

@ -90,7 +90,7 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
})
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Uninstall {
/// The number of files that were removed during the uninstallation.
pub file_count: usize,

View file

@ -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)]

View file

@ -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<ExitStatus> {
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::<Result<Vec<Requirement>>>()?;
// 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::<Vec<_>>();
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::<Vec<_>>();
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)
}

View file

@ -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<ExitStatus> {
// 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)
}

View file

@ -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<String>,
/// Uninstall all packages listed in the given requirements files.
#[clap(short, long, group = "sources")]
requirement: Vec<PathBuf>,
}
#[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::<Vec<_>>();
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),

View file

@ -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<String> for RequirementsSource {
fn from(name: String) -> Self {
Self::Name(name)
}
}
impl From<PathBuf> 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<impl Iterator<Item = Requirement>> {
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),
))
}
}
}
}