mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35: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 {
|
pub struct Uninstall {
|
||||||
/// The number of files that were removed during the uninstallation.
|
/// The number of files that were removed during the uninstallation.
|
||||||
pub file_count: usize,
|
pub file_count: usize,
|
||||||
|
|
|
@ -6,8 +6,8 @@ pub(crate) use clean::clean;
|
||||||
pub(crate) use freeze::freeze;
|
pub(crate) use freeze::freeze;
|
||||||
pub(crate) use pip_compile::pip_compile;
|
pub(crate) use pip_compile::pip_compile;
|
||||||
pub(crate) use pip_sync::{pip_sync, PipSyncFlags};
|
pub(crate) use pip_sync::{pip_sync, PipSyncFlags};
|
||||||
|
pub(crate) use pip_uninstall::pip_uninstall;
|
||||||
pub(crate) use remove::remove;
|
pub(crate) use remove::remove;
|
||||||
pub(crate) use uninstall::uninstall;
|
|
||||||
pub(crate) use venv::venv;
|
pub(crate) use venv::venv;
|
||||||
|
|
||||||
mod add;
|
mod add;
|
||||||
|
@ -15,9 +15,9 @@ mod clean;
|
||||||
mod freeze;
|
mod freeze;
|
||||||
mod pip_compile;
|
mod pip_compile;
|
||||||
mod pip_sync;
|
mod pip_sync;
|
||||||
|
mod pip_uninstall;
|
||||||
mod remove;
|
mod remove;
|
||||||
mod reporters;
|
mod reporters;
|
||||||
mod uninstall;
|
|
||||||
mod venv;
|
mod venv;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[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 owo_colors::OwoColorize;
|
||||||
|
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
|
use crate::requirements::RequirementsSource;
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod printer;
|
mod printer;
|
||||||
|
mod requirements;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about)]
|
#[command(author, version, about)]
|
||||||
|
@ -41,8 +43,8 @@ enum Commands {
|
||||||
Clean,
|
Clean,
|
||||||
/// Enumerate the installed packages in the current environment.
|
/// Enumerate the installed packages in the current environment.
|
||||||
Freeze,
|
Freeze,
|
||||||
/// Uninstall a package.
|
/// Uninstall packages from the current environment.
|
||||||
Uninstall(UninstallArgs),
|
PipUninstall(PipUninstallArgs),
|
||||||
/// Create a virtual environment.
|
/// Create a virtual environment.
|
||||||
Venv(VenvArgs),
|
Venv(VenvArgs),
|
||||||
/// Add a dependency to the workspace.
|
/// Add a dependency to the workspace.
|
||||||
|
@ -71,9 +73,15 @@ struct PipSyncArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
struct UninstallArgs {
|
#[command(group = clap::ArgGroup::new("sources").required(true))]
|
||||||
/// The name of the package to uninstall.
|
struct PipUninstallArgs {
|
||||||
name: String,
|
/// 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)]
|
#[derive(Args)]
|
||||||
|
@ -119,7 +127,7 @@ async fn main() -> ExitCode {
|
||||||
|
|
||||||
let dirs = ProjectDirs::from("", "", "puffin");
|
let dirs = ProjectDirs::from("", "", "puffin");
|
||||||
|
|
||||||
let result = match &cli.command {
|
let result = match cli.command {
|
||||||
Commands::PipCompile(args) => {
|
Commands::PipCompile(args) => {
|
||||||
commands::pip_compile(
|
commands::pip_compile(
|
||||||
&args.src,
|
&args.src,
|
||||||
|
@ -146,11 +154,15 @@ async fn main() -> ExitCode {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::Clean => {
|
Commands::PipUninstall(args) => {
|
||||||
commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await
|
let sources = args
|
||||||
}
|
.package
|
||||||
Commands::Freeze => {
|
.into_iter()
|
||||||
commands::freeze(
|
.map(RequirementsSource::from)
|
||||||
|
.chain(args.requirement.into_iter().map(RequirementsSource::from))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
commands::pip_uninstall(
|
||||||
|
&sources,
|
||||||
dirs.as_ref()
|
dirs.as_ref()
|
||||||
.map(ProjectDirs::cache_dir)
|
.map(ProjectDirs::cache_dir)
|
||||||
.filter(|_| !cli.no_cache),
|
.filter(|_| !cli.no_cache),
|
||||||
|
@ -158,9 +170,11 @@ async fn main() -> ExitCode {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::Uninstall(args) => {
|
Commands::Clean => {
|
||||||
commands::uninstall(
|
commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await
|
||||||
&args.name,
|
}
|
||||||
|
Commands::Freeze => {
|
||||||
|
commands::freeze(
|
||||||
dirs.as_ref()
|
dirs.as_ref()
|
||||||
.map(ProjectDirs::cache_dir)
|
.map(ProjectDirs::cache_dir)
|
||||||
.filter(|_| !cli.no_cache),
|
.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