Add a puffin remove command (#120)

This commit is contained in:
Charlie Marsh 2023-10-18 14:50:08 -04:00 committed by GitHub
parent 1fc03780f9
commit 2d14c0647e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 8 deletions

View file

@ -5,6 +5,7 @@ pub(crate) use add::add;
pub(crate) use clean::clean;
pub(crate) use compile::compile;
pub(crate) use freeze::freeze;
pub(crate) use remove::remove;
pub(crate) use sync::{sync, SyncFlags};
pub(crate) use uninstall::uninstall;
pub(crate) use venv::venv;
@ -13,6 +14,7 @@ mod add;
mod clean;
mod compile;
mod freeze;
mod remove;
mod reporters;
mod sync;
mod uninstall;

View file

@ -0,0 +1,73 @@
use std::path::PathBuf;
use anyhow::Result;
use miette::{Diagnostic, IntoDiagnostic};
use thiserror::Error;
use tracing::info;
use puffin_workspace::WorkspaceError;
use crate::commands::ExitStatus;
use crate::printer::Printer;
/// Remove a dependency from the workspace.
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn remove(name: &str, _printer: Printer) -> Result<ExitStatus> {
match remove_impl(name) {
Ok(status) => Ok(status),
Err(err) => {
#[allow(clippy::print_stderr)]
{
eprint!("{err:?}");
}
Ok(ExitStatus::Failure)
}
}
}
#[derive(Error, Debug, Diagnostic)]
enum RemoveError {
#[error(
"Could not find a `pyproject.toml` file in the current directory or any of its parents"
)]
#[diagnostic(code(puffin::remove::workspace_not_found))]
WorkspaceNotFound,
#[error("Failed to parse `pyproject.toml` at: `{0}`")]
#[diagnostic(code(puffin::remove::parse_error))]
ParseError(PathBuf, #[source] WorkspaceError),
#[error("Failed to write `pyproject.toml` to: `{0}`")]
#[diagnostic(code(puffin::remove::write_error))]
WriteError(PathBuf, #[source] WorkspaceError),
#[error("Failed to remove `{0}` from `pyproject.toml`")]
#[diagnostic(code(puffin::remove::parse_error))]
RemovalError(String, #[source] WorkspaceError),
}
fn remove_impl(name: &str) -> miette::Result<ExitStatus> {
// Locate the workspace.
let cwd = std::env::current_dir().into_diagnostic()?;
let Some(workspace_root) = puffin_workspace::find_pyproject_toml(cwd) else {
return Err(RemoveError::WorkspaceNotFound.into());
};
info!("Found workspace at: {}", workspace_root.display());
// Parse the manifest.
let mut manifest = puffin_workspace::Workspace::try_from(workspace_root.as_path())
.map_err(|err| RemoveError::ParseError(workspace_root.clone(), err))?;
// Remove the dependency.
manifest
.remove_dependency(name)
.map_err(|err| RemoveError::RemovalError(name.to_string(), err))?;
// Write the manifest back to disk.
manifest
.save(&workspace_root)
.map_err(|err| RemoveError::WriteError(workspace_root.clone(), err))?;
Ok(ExitStatus::Success)
}

View file

@ -1,7 +1,7 @@
use std::fmt::Write;
use std::path::Path;
use anyhow::{Context, Result};
use anyhow::Result;
use colored::Colorize;
use fs_err::tokio as fs;
@ -29,8 +29,8 @@ pub(crate) async fn venv(
)?;
// If the path already exists, remove it.
fs::remove_file(path).await.context("Foo")?;
fs::remove_dir_all(path).await?;
fs::remove_file(path).await.ok();
fs::remove_dir_all(path).await.ok();
writeln!(
printer,

View file

@ -47,6 +47,8 @@ enum Commands {
Venv(VenvArgs),
/// Add a dependency to the workspace.
Add(AddArgs),
/// Remove a dependency from the workspace.
Remove(RemoveArgs),
}
#[derive(Args)]
@ -87,7 +89,13 @@ struct VenvArgs {
#[derive(Args)]
struct AddArgs {
/// The name of the package to add.
/// The name of the package to add (e.g., `Django==4.2.6`).
name: String,
}
#[derive(Args)]
struct RemoveArgs {
/// The name of the package to remove (e.g., `Django`).
name: String,
}
@ -162,6 +170,7 @@ async fn main() -> ExitCode {
}
Commands::Venv(args) => commands::venv(&args.name, args.python.as_deref(), printer).await,
Commands::Add(args) => commands::add(&args.name, printer),
Commands::Remove(args) => commands::remove(&args.name, printer),
};
match result {

View file

@ -14,4 +14,13 @@ pub enum WorkspaceError {
#[error(transparent)]
InvalidRequirement(#[from] pep508_rs::Pep508Error),
#[error("no `[project]` table found in `pyproject.toml`")]
MissingProjectTable,
#[error("no `[project.dependencies]` array found in `pyproject.toml`")]
MissingProjectDependenciesArray,
#[error("unable to find package: `{0}`")]
MissingPackage(String),
}

View file

@ -1,5 +1,10 @@
/// Reformat a TOML array to use multiline format.
pub(crate) fn format_multiline_array(dependencies: &mut toml_edit::Array) {
if dependencies.is_empty() {
dependencies.set_trailing("");
return;
}
for item in dependencies.iter_mut() {
let decor = item.decor_mut();
let mut prefix = String::new();

View file

@ -76,9 +76,7 @@ impl Workspace {
return;
};
// TODO(charlie): Awkward `drop` pattern required to work around destructors, apparently.
let mut iter = dependencies.iter();
let index = iter.position(|item| {
let index = dependencies.iter().position(|item| {
let Some(item) = item.as_str() else {
return false;
};
@ -90,7 +88,6 @@ impl Workspace {
PackageName::normalize(&requirement.requirement.name)
== PackageName::normalize(existing.name)
});
drop(iter);
if let Some(index) = index {
dependencies.replace(index, requirement.given_name);
@ -101,6 +98,45 @@ impl Workspace {
format_multiline_array(dependencies);
}
/// Remove a dependency from the workspace.
pub fn remove_dependency(&mut self, name: &str) -> Result<(), WorkspaceError> {
let Some(project) = self
.document
.get_mut("project")
.map(|project| project.as_table_mut().unwrap())
else {
return Err(WorkspaceError::MissingProjectTable);
};
let Some(dependencies) = project
.get_mut("dependencies")
.map(|dependencies| dependencies.as_array_mut().unwrap())
else {
return Err(WorkspaceError::MissingProjectDependenciesArray);
};
let index = dependencies.iter().position(|item| {
let Some(item) = item.as_str() else {
return false;
};
let Ok(existing) = Requirement::from_str(item) else {
return false;
};
PackageName::normalize(name) == PackageName::normalize(existing.name)
});
let Some(index) = index else {
return Err(WorkspaceError::MissingPackage(name.to_string()));
};
dependencies.remove(index);
format_multiline_array(dependencies);
Ok(())
}
/// Save the workspace to disk.
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), WorkspaceError> {
let file = fs::File::create(path.as_ref())?;