Skip to content

Commit

Permalink
uv remove should remove all packages with matching name
Browse files Browse the repository at this point in the history
  • Loading branch information
blueraft committed Aug 12, 2024
1 parent 0fbde82 commit d104164
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 48 deletions.
56 changes: 29 additions & 27 deletions crates/uv-workspace/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::str::FromStr;
use std::{fmt, mem};

use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl};
use pep508_rs::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl};
use thiserror::Error;
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
use uv_fs::PortablePath;
Expand Down Expand Up @@ -343,10 +343,7 @@ impl PyProjectTomlMut {
}

/// Removes all occurrences of dependencies with the given name.
pub fn remove_dependency(
&mut self,
requirement: &Requirement,
) -> Result<Vec<Requirement>, Error> {
pub fn remove_dependency(&mut self, name: &PackageName) -> Result<Vec<Requirement>, Error> {
// Try to get `project.dependencies`.
let Some(dependencies) = self
.doc_mut()?
Expand All @@ -357,17 +354,14 @@ impl PyProjectTomlMut {
return Ok(Vec::new());
};

let requirements = remove_dependency(requirement, dependencies);
self.remove_source(requirement)?;
let requirements = remove_dependency(name, dependencies);
self.remove_source(name)?;

Ok(requirements)
}

/// Removes all occurrences of development dependencies with the given name.
pub fn remove_dev_dependency(
&mut self,
requirement: &Requirement,
) -> Result<Vec<Requirement>, Error> {
pub fn remove_dev_dependency(&mut self, name: &PackageName) -> Result<Vec<Requirement>, Error> {
// Try to get `tool.uv.dev-dependencies`.
let Some(dev_dependencies) = self
.doc
Expand All @@ -384,16 +378,16 @@ impl PyProjectTomlMut {
return Ok(Vec::new());
};

let requirements = remove_dependency(requirement, dev_dependencies);
self.remove_source(requirement)?;
let requirements = remove_dependency(name, dev_dependencies);
self.remove_source(name)?;

Ok(requirements)
}

/// Removes all occurrences of optional dependencies in the group with the given name.
pub fn remove_optional_dependency(
&mut self,
requirement: &Requirement,
name: &PackageName,
group: &ExtraName,
) -> Result<Vec<Requirement>, Error> {
// Try to get `project.optional-dependencies.<group>`.
Expand All @@ -409,14 +403,14 @@ impl PyProjectTomlMut {
return Ok(Vec::new());
};

let requirements = remove_dependency(requirement, optional_dependencies);
self.remove_source(requirement)?;
let requirements = remove_dependency(name, optional_dependencies);
self.remove_source(name)?;

Ok(requirements)
}

/// Remove a matching source from `tool.uv.sources`, if it exists.
fn remove_source(&mut self, requirement: &Requirement) -> Result<(), Error> {
fn remove_source(&mut self, name: &PackageName) -> Result<(), Error> {
if let Some(sources) = self
.doc
.get_mut("tool")
Expand All @@ -429,7 +423,7 @@ impl PyProjectTomlMut {
.map(|sources| sources.as_table_mut().ok_or(Error::MalformedSources))
.transpose()?
{
sources.remove(requirement.name.as_ref());
sources.remove(name.as_ref());
}

Ok(())
Expand All @@ -440,13 +434,17 @@ impl PyProjectTomlMut {
///
/// This method searches `project.dependencies`, `tool.uv.dev-dependencies`, and
/// `tool.uv.optional-dependencies`.
pub fn find_dependency(&self, requirement: &Requirement) -> Vec<DependencyType> {
pub fn find_dependency(
&self,
name: &PackageName,
marker: Option<&MarkerTree>,
) -> Vec<DependencyType> {
let mut types = Vec::new();

if let Some(project) = self.doc.get("project").and_then(Item::as_table) {
// Check `project.dependencies`.
if let Some(dependencies) = project.get("dependencies").and_then(Item::as_array) {
if !find_dependencies(requirement, dependencies).is_empty() {
if !find_dependencies(name, marker, dependencies).is_empty() {
types.push(DependencyType::Production);
}
}
Expand All @@ -464,7 +462,7 @@ impl PyProjectTomlMut {
continue;
};

if !find_dependencies(requirement, dependencies).is_empty() {
if !find_dependencies(name, marker, dependencies).is_empty() {
types.push(DependencyType::Optional(extra));
}
}
Expand All @@ -481,7 +479,7 @@ impl PyProjectTomlMut {
.and_then(|tool| tool.get("dev-dependencies"))
.and_then(Item::as_array)
{
if !find_dependencies(requirement, dev_dependencies).is_empty() {
if !find_dependencies(name, marker, dev_dependencies).is_empty() {
types.push(DependencyType::Dev);
}
}
Expand All @@ -506,7 +504,7 @@ pub fn add_dependency(
has_source: bool,
) -> Result<ArrayEdit, Error> {
// Find matching dependencies.
let mut to_replace = find_dependencies(req, deps);
let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps);
match to_replace.as_slice() {
[] => {
deps.push(req.to_string());
Expand Down Expand Up @@ -551,9 +549,9 @@ fn update_requirement(old: &mut Requirement, new: &Requirement, has_source: bool
}

/// Removes all occurrences of dependencies with the given name from the given `deps` array.
fn remove_dependency(req: &Requirement, deps: &mut Array) -> Vec<Requirement> {
fn remove_dependency(name: &PackageName, deps: &mut Array) -> Vec<Requirement> {
// Remove matching dependencies.
let removed = find_dependencies(req, deps)
let removed = find_dependencies(name, None, deps)
.into_iter()
.rev() // Reverse to preserve indices as we remove them.
.filter_map(|(i, _)| {
Expand All @@ -572,11 +570,15 @@ fn remove_dependency(req: &Requirement, deps: &mut Array) -> Vec<Requirement> {

/// Returns a `Vec` containing the all dependencies with the given name, along with their positions
/// in the array.
fn find_dependencies(requirement: &Requirement, deps: &Array) -> Vec<(usize, Requirement)> {
fn find_dependencies(
name: &PackageName,
marker: Option<&MarkerTree>,
deps: &Array,
) -> Vec<(usize, Requirement)> {
let mut to_replace = Vec::new();
for (i, dep) in deps.iter().enumerate() {
if let Some(req) = dep.as_str().and_then(try_parse_requirement) {
if req.name == requirement.name && req.marker == requirement.marker {
if marker.map_or(true, |m| *m == req.marker) && *name == req.name {
to_replace.push((i, req));
}
}
Expand Down
29 changes: 12 additions & 17 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use std::str::FromStr;

use anyhow::{Context, Result};

use pep508_rs::{PackageName, Requirement};
use pep508_rs::PackageName;
use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
Expand Down Expand Up @@ -95,30 +93,29 @@ pub(crate) async fn remove(
}?;

for package in packages {
let requirement = Requirement::from_str(package.as_str())?;
match dependency_type {
DependencyType::Production => {
let deps = toml.remove_dependency(&requirement)?;
let deps = toml.remove_dependency(&package)?;
if deps.is_empty() {
warn_if_present(&requirement, &toml);
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `dependencies`"
);
}
}
DependencyType::Dev => {
let deps = toml.remove_dev_dependency(&requirement)?;
let deps = toml.remove_dev_dependency(&package)?;
if deps.is_empty() {
warn_if_present(&requirement, &toml);
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `dev-dependencies`"
);
}
}
DependencyType::Optional(ref group) => {
let deps = toml.remove_optional_dependency(&requirement, group)?;
let deps = toml.remove_optional_dependency(&package, group)?;
if deps.is_empty() {
warn_if_present(&requirement, &toml);
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `optional-dependencies`"
);
Expand Down Expand Up @@ -226,22 +223,20 @@ enum Target {
///
/// This is useful when a dependency of the user-specified type was not found, but it may be present
/// elsewhere.
fn warn_if_present(requirement: &Requirement, pyproject: &PyProjectTomlMut) {
for dep_ty in pyproject.find_dependency(requirement) {
fn warn_if_present(requirement: &PackageName, pyproject: &PyProjectTomlMut) {
for dep_ty in pyproject.find_dependency(requirement, None) {
match dep_ty {
DependencyType::Production => {
warn_user!("`{}` is a production dependency", requirement.name);
warn_user!("`{requirement}` is a production dependency");
}
DependencyType::Dev => {
warn_user!(
"`{}` is a development dependency; try calling `uv remove --dev`",
requirement.name
"`{requirement}` is a development dependency; try calling `uv remove --dev`",
);
}
DependencyType::Optional(group) => {
warn_user!(
"`{}` is an optional dependency; try calling `uv remove --optional {group}`",
requirement.name
"`{requirement}` is an optional dependency; try calling `uv remove --optional {group}`",
);
}
}
Expand Down
104 changes: 100 additions & 4 deletions crates/uv/tests/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ fn add_raw_error() -> Result<()> {
----- stderr -----
error: the argument '--tag <TAG>' cannot be used with '--raw-sources'
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <REQUIREMENTS>...
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <PACKAGES>...
For more information, try '--help'.
"###);
Expand Down Expand Up @@ -1812,6 +1812,42 @@ fn update_marker() -> Result<()> {
);
});

uv_snapshot!(context.filters(), context.remove(&["requests"]), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv remove` is experimental and may change without warning
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Uninstalled 6 packages in [TIME]
Installed 1 package in [TIME]
- certifi==2024.2.2
- charset-normalizer==3.3.2
- idna==3.6
- project==0.1.0 (from file://[TEMP_DIR]/)
+ project==0.1.0 (from file://[TEMP_DIR]/)
- requests==2.31.0
- urllib3==2.2.1
"###);

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.8"
dependencies = []
"###
);
});

Ok(())
}

Expand Down Expand Up @@ -2358,7 +2394,7 @@ fn add_reject_multiple_git_ref_flags() {
----- stderr -----
error: the argument '--tag <TAG>' cannot be used with '--branch <BRANCH>'
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <REQUIREMENTS>...
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <PACKAGES>...
For more information, try '--help'.
"###
Expand All @@ -2379,7 +2415,7 @@ fn add_reject_multiple_git_ref_flags() {
----- stderr -----
error: the argument '--tag <TAG>' cannot be used with '--rev <REV>'
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <REQUIREMENTS>...
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <PACKAGES>...
For more information, try '--help'.
"###
Expand All @@ -2400,7 +2436,7 @@ fn add_reject_multiple_git_ref_flags() {
----- stderr -----
error: the argument '--tag <TAG>' cannot be used multiple times
Usage: uv add [OPTIONS] <REQUIREMENTS>...
Usage: uv add [OPTIONS] [PACKAGES]...
For more information, try '--help'.
"###
Expand Down Expand Up @@ -3031,6 +3067,66 @@ fn add_repeat() -> Result<()> {
Ok(())
}

/// Add from requirement file.
#[test]
fn add_requirements_file() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();

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 = []
"#})?;

let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("Flask==2.3.2")?;

uv_snapshot!(context.filters(), context.add(&[]).arg("-r").arg("requirements.txt"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv add` is experimental and may change without warning
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ blinker==1.7.0
+ click==8.1.7
+ flask==2.3.2
+ itsdangerous==2.1.2
+ jinja2==3.1.3
+ markupsafe==2.1.5
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ werkzeug==3.0.1
"###);

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 = [
"flask==2.3.2",
]
"###
);
});

Ok(())
}

/// Add to a PEP 732 script.
#[test]
fn add_script() -> Result<()> {
Expand Down

0 comments on commit d104164

Please sign in to comment.