diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 0a391531e0e2..cfb084fea45f 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -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; @@ -343,10 +343,7 @@ impl PyProjectTomlMut { } /// Removes all occurrences of dependencies with the given name. - pub fn remove_dependency( - &mut self, - requirement: &Requirement, - ) -> Result, Error> { + pub fn remove_dependency(&mut self, name: &PackageName) -> Result, Error> { // Try to get `project.dependencies`. let Some(dependencies) = self .doc_mut()? @@ -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, Error> { + pub fn remove_dev_dependency(&mut self, name: &PackageName) -> Result, Error> { // Try to get `tool.uv.dev-dependencies`. let Some(dev_dependencies) = self .doc @@ -384,8 +378,8 @@ 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) } @@ -393,7 +387,7 @@ impl PyProjectTomlMut { /// 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, Error> { // Try to get `project.optional-dependencies.`. @@ -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") @@ -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(()) @@ -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 { + pub fn find_dependency( + &self, + name: &PackageName, + marker: Option<&MarkerTree>, + ) -> Vec { 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); } } @@ -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)); } } @@ -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); } } @@ -506,7 +504,7 @@ pub fn add_dependency( has_source: bool, ) -> Result { // 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()); @@ -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 { +fn remove_dependency(name: &PackageName, deps: &mut Array) -> Vec { // 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, _)| { @@ -572,11 +570,15 @@ fn remove_dependency(req: &Requirement, deps: &mut Array) -> Vec { /// 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)); } } diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 8c0411642746..757cbee1bcf2 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -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}; @@ -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`" ); @@ -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}`", ); } } diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index b48667068b72..4bd0e8260cfc 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -583,7 +583,7 @@ fn add_raw_error() -> Result<()> { ----- stderr ----- error: the argument '--tag ' cannot be used with '--raw-sources' - Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... + Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... For more information, try '--help'. "###); @@ -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(()) } @@ -2358,7 +2394,7 @@ fn add_reject_multiple_git_ref_flags() { ----- stderr ----- error: the argument '--tag ' cannot be used with '--branch ' - Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... + Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... For more information, try '--help'. "### @@ -2379,7 +2415,7 @@ fn add_reject_multiple_git_ref_flags() { ----- stderr ----- error: the argument '--tag ' cannot be used with '--rev ' - Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... + Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... For more information, try '--help'. "### @@ -2400,7 +2436,7 @@ fn add_reject_multiple_git_ref_flags() { ----- stderr ----- error: the argument '--tag ' cannot be used multiple times - Usage: uv add [OPTIONS] ... + Usage: uv add [OPTIONS] [PACKAGES]... For more information, try '--help'. "### @@ -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<()> {