Skip to content

Commit

Permalink
remove all occurences of matching dependencies in uv remove
Browse files Browse the repository at this point in the history
  • Loading branch information
ibraheemdev committed Jun 11, 2024
1 parent 3520357 commit a955b35
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 24 deletions.
37 changes: 20 additions & 17 deletions crates/uv-distribution/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,37 +64,40 @@ impl PyProjectTomlMut {
Ok(())
}

/// Removes a dependency.
pub fn remove_dependency(&mut self, req: &Requirement) -> Result<Option<Requirement>, Error> {
/// Removes all occurrences of dependencies with the given name.
pub fn remove_dependency(&mut self, req: &String) -> Result<Vec<Requirement>, Error> {
let deps = &mut self.doc["project"]["dependencies"];
if deps.is_none() {
return Ok(None);
return Ok(Vec::new());
}

let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;

// Try to find a matching dependency.
let mut to_remove = None;
// Try to find matching dependencies.
let mut to_remove = Vec::new();
for (i, dep) in deps.iter().enumerate() {
if let Some(dep) = dep.as_str().and_then(try_parse_requirement) {
if dep.name.as_ref().eq_ignore_ascii_case(req.name.as_ref()) {
to_remove = Some(i);
break;
if dep.name.as_ref().eq_ignore_ascii_case(req.as_ref()) {
to_remove.push(i);
}
}
}

match to_remove {
Some(i) => {
let req = deps
.remove(i)
let removed = to_remove
.into_iter()
.rev() // Reverse to preserve indices as we remove them.
.filter_map(|i| {
deps.remove(i)
.as_str()
.and_then(|req| Requirement::from_str(req).ok());
reformat_array_multiline(deps);
Ok(req)
}
None => Ok(None),
.and_then(|req| Requirement::from_str(req).ok())
})
.collect::<Vec<_>>();

if !removed.is_empty() {
reformat_array_multiline(deps);
}

Ok(removed)
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1952,7 +1952,7 @@ pub(crate) struct AddArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct RemoveArgs {
/// The packages to remove, as PEP 508 requirements (e.g., `flask==2.2.3`).
/// The names of the packages to remove (e.g., `flask`).
#[arg(required = true)]
pub(crate) requirements: Vec<String>,

Expand Down
8 changes: 2 additions & 6 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use anyhow::Result;
use std::str::FromStr;
use uv_distribution::pyproject_mut::PyProjectTomlMut;

use distribution_types::IndexLocations;
use pep508_rs::Requirement;
use pypi_types::LenientRequirement;
use uv_cache::Cache;
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
use uv_distribution::ProjectWorkspace;
Expand All @@ -31,11 +28,10 @@ pub(crate) async fn remove(

let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
for req in requirements {
let req = Requirement::from(LenientRequirement::from_str(&req)?);
if pyproject.remove_dependency(&req)?.is_none() {
if pyproject.remove_dependency(&req)?.is_empty() {
anyhow::bail!(
"The dependency `{}` could not be found in `dependencies`",
req.name
req
);
}
}
Expand Down
106 changes: 106 additions & 0 deletions crates/uv/tests/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,109 @@ fn remove_registry() -> Result<()> {

Ok(())
}

/// Remove a PyPI requirement that occurs multiple times.
#[test]
fn remove_all_registry() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"anyio == 3.7.0 ; python_version >= '3.12'",
"anyio < 3.7.0 ; python_version < '3.12'",
]
"#})?;

uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv lock` is experimental and may change without warning.
Resolved 10 packages in [TIME]
"###);

uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);

uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv remove` is experimental and may change without warning.
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- project==0.1.0 (from file://[TEMP_DIR]/)
+ project==0.1.0 (from file://[TEMP_DIR]/)
"###);

let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
"###
);
});

let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
[[distribution]]
name = "project"
version = "0.1.0"
source = "editable+file://[TEMP_DIR]/"
sdist = { url = "file://[TEMP_DIR]/" }
"###
);
});

// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
Audited 1 package in [TIME]
"###);

Ok(())
}

0 comments on commit a955b35

Please sign in to comment.