mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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:
parent
4b91ae4769
commit
573f5832a3
6 changed files with 204 additions and 71 deletions
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
124
crates/puffin-cli/src/commands/pip_uninstall.rs
Normal file
124
crates/puffin-cli/src/commands/pip_uninstall.rs
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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),
|
||||
|
|
49
crates/puffin-cli/src/requirements.rs
Normal file
49
crates/puffin-cli/src/requirements.rs
Normal 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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue