diff --git a/crates/uv-distribution/src/pyproject_mut.rs b/crates/uv-distribution/src/pyproject_mut.rs index 917cf9d80e0c..e35c82aa0973 100644 --- a/crates/uv-distribution/src/pyproject_mut.rs +++ b/crates/uv-distribution/src/pyproject_mut.rs @@ -2,12 +2,12 @@ use std::fmt; use std::str::FromStr; use thiserror::Error; -use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value}; +use toml_edit::{Array, DocumentMut, InlineTable, Item, RawString, Table, TomlError, Value}; use pep508_rs::{PackageName, Requirement}; use pypi_types::VerbatimParsedUrl; -use crate::pyproject::PyProjectToml; +use crate::pyproject::{PyProjectToml, Source}; /// Raw and mutable representation of a `pyproject.toml`. /// @@ -23,6 +23,8 @@ pub enum Error { Parse(#[from] Box), #[error("Dependencies in `pyproject.toml` are malformed")] MalformedDependencies, + #[error("Sources in `pyproject.toml` are malformed")] + MalformedSources, } impl PyProjectTomlMut { @@ -34,47 +36,165 @@ impl PyProjectTomlMut { } /// Adds a dependency to `project.dependencies`. - pub fn add_dependency(&mut self, req: &Requirement) -> Result<(), Error> { - add_dependency(req, &mut self.doc["project"]["dependencies"]) + pub fn add_dependency( + &mut self, + req: &Requirement, + source: Option<&Source>, + ) -> Result<(), Error> { + // Get or create `project.dependencies`. + let dependencies = self + .doc + .entry("project") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .ok_or(Error::MalformedDependencies)? + .entry("dependencies") + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + add_dependency(req, dependencies); + + if let Some(source) = source { + // Get or create `tool.uv.sources`. + let sources = self + .doc + .entry("tool") + .or_insert(implicit()) + .as_table_mut() + .ok_or(Error::MalformedSources)? + .entry("uv") + .or_insert(implicit()) + .as_table_mut() + .ok_or(Error::MalformedSources)? + .entry("sources") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .ok_or(Error::MalformedSources)?; + + add_source(req, source, sources); + } + + Ok(()) } /// Adds a development dependency to `tool.uv.dev-dependencies`. - pub fn add_dev_dependency(&mut self, req: &Requirement) -> Result<(), Error> { - let tool = self.doc["tool"].or_insert({ - let mut tool = Table::new(); - tool.set_implicit(true); - Item::Table(tool) - }); - let tool_uv = tool["uv"].or_insert(Item::Table(Table::new())); - - add_dependency(req, &mut tool_uv["dev-dependencies"]) + pub fn add_dev_dependency( + &mut self, + req: &Requirement, + source: Option<&Source>, + ) -> Result<(), Error> { + // Get or create `tool.uv`. + let tool_uv = self + .doc + .entry("tool") + .or_insert(implicit()) + .as_table_mut() + .ok_or(Error::MalformedSources)? + .entry("uv") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .ok_or(Error::MalformedSources)?; + + // Get or create the `tool.uv.dev-dependencies` array. + let dev_dependencies = tool_uv + .entry("dev-dependencies") + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + add_dependency(req, dev_dependencies); + + if let Some(source) = source { + // Get or create `tool.uv.sources`. + let sources = tool_uv + .entry("sources") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .ok_or(Error::MalformedSources)?; + + add_source(req, source, sources); + } + + Ok(()) } /// Removes all occurrences of dependencies with the given name. pub fn remove_dependency(&mut self, req: &PackageName) -> Result, Error> { - remove_dependency(req, &mut self.doc["project"]["dependencies"]) + // Try to get `project.dependencies`. + let Some(dependencies) = self + .doc + .get_mut("project") + .and_then(Item::as_table_mut) + .and_then(|project| project.get_mut("dependencies")) + else { + return Ok(Vec::new()); + }; + let dependencies = dependencies + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + let requirements = remove_dependency(req, dependencies); + + // Remove a matching source from `tool.uv.sources`, if it exists. + if let Some(sources) = self + .doc + .get_mut("tool") + .and_then(Item::as_table_mut) + .and_then(|tool| tool.get_mut("uv")) + .and_then(Item::as_table_mut) + .and_then(|tool_uv| tool_uv.get_mut("sources")) + { + let sources = sources.as_table_mut().ok_or(Error::MalformedSources)?; + sources.remove(req.as_ref()); + } + + Ok(requirements) } /// Removes all occurrences of development dependencies with the given name. pub fn remove_dev_dependency(&mut self, req: &PackageName) -> Result, Error> { - let Some(tool_uv) = self.doc.get_mut("tool").and_then(|tool| tool.get_mut("uv")) else { + let Some(tool_uv) = self + .doc + .get_mut("tool") + .and_then(Item::as_table_mut) + .and_then(|tool| tool.get_mut("uv")) + .and_then(Item::as_table_mut) + else { return Ok(Vec::new()); }; - remove_dependency(req, &mut tool_uv["dev-dependencies"]) + // Try to get `tool.uv.dev-dependencies`. + let Some(dev_dependencies) = tool_uv.get_mut("dev-dependencies") else { + return Ok(Vec::new()); + }; + let dev_dependencies = dev_dependencies + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + let requirements = remove_dependency(req, dev_dependencies); + + // Remove a matching source from `tool.uv.sources`, if it exists. + if let Some(sources) = tool_uv.get_mut("sources") { + let sources = sources.as_table_mut().ok_or(Error::MalformedSources)?; + sources.remove(req.as_ref()); + }; + + Ok(requirements) } } -/// Adds a dependency to the given `deps` array. -pub fn add_dependency(req: &Requirement, deps: &mut Item) -> Result<(), Error> { - let deps = deps - .or_insert(Item::Value(Value::Array(Array::new()))) - .as_array_mut() - .ok_or(Error::MalformedDependencies)?; +/// Returns an implicit table. +fn implicit() -> Item { + let mut table = Table::new(); + table.set_implicit(true); + Item::Table(table) +} +/// Adds a dependency to the given `deps` array. +pub fn add_dependency(req: &Requirement, deps: &mut Array) { // Find matching dependencies. let to_replace = find_dependencies(&req.name, deps); - if to_replace.is_empty() { deps.push(req.to_string()); } else { @@ -84,19 +204,11 @@ pub fn add_dependency(req: &Requirement, deps: &mut Item) -> Result<(), Error> { deps.remove(i); } } - reformat_array_multiline(deps); - Ok(()) } /// Removes all occurrences of dependencies with the given name from the given `deps` array. -fn remove_dependency(req: &PackageName, deps: &mut Item) -> Result, Error> { - if deps.is_none() { - return Ok(Vec::new()); - } - - let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?; - +fn remove_dependency(req: &PackageName, deps: &mut Array) -> Vec { // Remove matching dependencies. let removed = find_dependencies(req, deps) .into_iter() @@ -112,7 +224,7 @@ fn remove_dependency(req: &PackageName, deps: &mut Item) -> Result Vec { to_replace } +// Add a source to `tool.uv.sources`. +fn add_source(req: &Requirement, source: &Source, sources: &mut Table) { + match source { + Source::Workspace { + workspace, + editable, + } => { + let mut value = InlineTable::new(); + value.insert("workspace", Value::from(*workspace)); + if let Some(editable) = editable { + value.insert("editable", Value::from(*editable)); + } + sources.insert(req.name.as_ref(), Item::Value(Value::InlineTable(value))); + } + _ => unimplemented!(), + } +} + impl fmt::Display for PyProjectTomlMut { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.doc.fmt(f) diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index 309a1a8dbdf8..1f26792ac3b0 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -1611,6 +1611,10 @@ pub(crate) struct AddArgs { #[arg(long)] pub(crate) dev: bool, + /// Add the requirements as workspace dependencies. + #[arg(long)] + pub(crate) workspace: bool, + #[command(flatten)] pub(crate) installer: ResolverInstallerArgs, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index eae2a395b2a6..62c14afdca14 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1,6 +1,7 @@ use anyhow::Result; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_dispatch::BuildDispatch; +use uv_distribution::pyproject::Source; use uv_distribution::pyproject_mut::PyProjectTomlMut; use uv_git::GitResolver; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; @@ -24,6 +25,7 @@ use crate::settings::ResolverInstallerSettings; #[allow(clippy::too_many_arguments)] pub(crate) async fn add( requirements: Vec, + workspace: bool, dev: bool, python: Option, settings: ResolverInstallerSettings, @@ -134,10 +136,19 @@ pub(crate) async fn add( // Add the requirements to the `pyproject.toml`. let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?; for req in requirements.into_iter().map(pep508_rs::Requirement::from) { + let source = if workspace { + Some(Source::Workspace { + workspace: true, + editable: None, + }) + } else { + None + }; + if dev { - pyproject.add_dev_dependency(&req)?; + pyproject.add_dev_dependency(&req, source.as_ref())?; } else { - pyproject.add_dependency(&req)?; + pyproject.add_dependency(&req, source.as_ref())?; } } diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index b7808da2a983..da097495a471 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -46,7 +46,7 @@ pub(crate) async fn remove( .filter(|deps| !deps.is_empty()) .is_some() { - uv_warnings::warn_user!("`{req}` is not a development dependency; try calling `uv add` without the `--dev` flag"); + uv_warnings::warn_user!("`{req}` is not a development dependency; try calling `uv remove` without the `--dev` flag"); } anyhow::bail!("The dependency `{req}` could not be found in `dev-dependencies`"); @@ -65,7 +65,7 @@ pub(crate) async fn remove( .is_some() { uv_warnings::warn_user!( - "`{req}` is a development dependency; try calling `uv add --dev`" + "`{req}` is a development dependency; try calling `uv remove --dev`" ); } diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 20b40fe1e8de..ea0f53f639ad 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -686,14 +686,9 @@ async fn run() -> Result { // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); - let requirements = args - .requirements - .into_iter() - .map(RequirementsSource::Package) - .collect::>(); - commands::add( - requirements, + args.requirements, + args.workspace, args.dev, args.python, args.settings, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index af5f5bf42f7a..abf9449b9252 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -17,6 +17,7 @@ use uv_configuration::{ Upgrade, }; use uv_normalize::PackageName; +use uv_requirements::RequirementsSource; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_settings::{ Combine, FilesystemOptions, InstallerOptions, Options, PipOptions, ResolverInstallerOptions, @@ -373,7 +374,8 @@ impl LockSettings { #[allow(clippy::struct_excessive_bools, dead_code)] #[derive(Debug, Clone)] pub(crate) struct AddSettings { - pub(crate) requirements: Vec, + pub(crate) requirements: Vec, + pub(crate) workspace: bool, pub(crate) dev: bool, pub(crate) python: Option, pub(crate) refresh: Refresh, @@ -387,14 +389,21 @@ impl AddSettings { let AddArgs { requirements, dev, + workspace, installer, build, refresh, python, } = args; + let requirements = requirements + .into_iter() + .map(RequirementsSource::Package) + .collect::>(); + Self { requirements, + workspace, dev, python, refresh: Refresh::from(refresh), diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 92c91d1373b8..3f899a555fdb 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -374,9 +374,9 @@ fn add_unnamed() -> Result<()> { Ok(()) } -/// Add a development dependency. +/// Add and remove a development dependency. #[test] -fn add_dev() -> Result<()> { +fn add_remove_dev() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -494,66 +494,35 @@ fn add_dev() -> Result<()> { Audited 4 packages in [TIME] "###); - Ok(()) -} - -/// Update a PyPI requirement. -#[test] -fn update_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 4 packages in [TIME] - "###); - - uv_snapshot!(context.filters(), context.sync(), @r###" - success: true - exit_code: 0 + // This should fail without --dev. + uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###" + success: false + exit_code: 2 ----- 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 + warning: `uv remove` is experimental and may change without warning. + warning: `anyio` is a development dependency; try calling `uv remove --dev` + error: The dependency `anyio` could not be found in `dependencies` "###); - uv_snapshot!(context.filters(), context.add(&["anyio==4.3.0"]), @r###" + // Remove the dependency. + uv_snapshot!(context.filters(), context.remove(&["anyio"]).arg("--dev"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - warning: `uv add` is experimental and may change without warning. - Resolved 4 packages in [TIME] - Downloaded 2 packages in [TIME] - Uninstalled 2 packages in [TIME] - Installed 2 packages in [TIME] + warning: `uv remove` is experimental and may change without warning. + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 4 packages in [TIME] + Installed 1 package in [TIME] - anyio==3.7.0 - + anyio==4.3.0 + - idna==3.7 - project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/) + - sniffio==1.3.1 "###); let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; @@ -567,9 +536,10 @@ fn update_registry() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [ - "anyio==4.3.0", - ] + dependencies = [] + + [tool.uv] + dev-dependencies = [] "### ); }); @@ -584,47 +554,11 @@ fn update_registry() -> Result<()> { version = 1 requires-python = ">=3.12" - [[distribution]] - name = "anyio" - version = "4.3.0" - source = "registry+https://pypi.org/simple" - sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } - wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }] - - [[distribution.dependencies]] - name = "idna" - version = "3.6" - source = "registry+https://pypi.org/simple" - - [[distribution.dependencies]] - name = "sniffio" - version = "1.3.1" - source = "registry+https://pypi.org/simple" - - [[distribution]] - name = "idna" - version = "3.6" - source = "registry+https://pypi.org/simple" - sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } - wheels = [{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }] - [[distribution]] name = "project" version = "0.1.0" source = "editable+." sdist = { path = "." } - - [[distribution.dependencies]] - name = "anyio" - version = "4.3.0" - source = "registry+https://pypi.org/simple" - - [[distribution]] - name = "sniffio" - version = "1.3.1" - source = "registry+https://pypi.org/simple" - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } - wheels = [{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }] "### ); }); @@ -637,79 +571,62 @@ fn update_registry() -> Result<()> { ----- stderr ----- warning: `uv sync` is experimental and may change without warning. - Audited 4 packages in [TIME] + Audited 1 package in [TIME] "###); Ok(()) } -/// Adding a dependency does not clean the environment. +/// Add and remove a workspace dependency. #[test] -fn add_no_clean() -> Result<()> { +fn add_remove_workspace() -> Result<()> { let context = TestContext::new("3.12"); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); + let workspace = context.temp_dir.child("pyproject.toml"); + workspace.write_str(indoc! {r#" + [tool.uv.workspace] + members = ["child1", "child2"] + "#})?; + + let pyproject_toml = context.temp_dir.child("child1/pyproject.toml"); pyproject_toml.write_str(indoc! {r#" [project] - name = "project" + name = "child1" version = "0.1.0" requires-python = ">=3.12" - dependencies = [ - "anyio == 3.7.0", - ] + dependencies = [] "#})?; - 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 4 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 - "###); - - // Manually remove a dependency. + let pyproject_toml = context.temp_dir.child("child2/pyproject.toml"); pyproject_toml.write_str(indoc! {r#" [project] - name = "project" + name = "child2" version = "0.1.0" requires-python = ">=3.12" dependencies = [] "#})?; - uv_snapshot!(context.filters(), context.add(&["iniconfig==2.0.0"]), @r###" + let child1 = context.temp_dir.join("child1"); + let mut add_cmd = context.add(&["child2"]); + add_cmd + .arg("--preview") + .arg("--workspace") + .current_dir(&child1); + + uv_snapshot!(context.filters(), add_cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - warning: `uv add` is experimental and may change without warning. Resolved 2 packages in [TIME] Downloaded 2 packages in [TIME] - Uninstalled 1 package in [TIME] Installed 2 packages in [TIME] - + iniconfig==2.0.0 - - project==0.1.0 (from file://[TEMP_DIR]/) - + project==0.1.0 (from file://[TEMP_DIR]/) + + child1==0.1.0 (from file://[TEMP_DIR]/child1) + + child2==0.1.0 (from file://[TEMP_DIR]/child2) "###); - let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; + let pyproject_toml = fs_err::read_to_string(child1.join("pyproject.toml"))?; insta::with_settings!({ filters => context.filters(), @@ -717,16 +634,20 @@ fn add_no_clean() -> Result<()> { assert_snapshot!( pyproject_toml, @r###" [project] - name = "project" + name = "child1" version = "0.1.0" requires-python = ">=3.12" dependencies = [ - "iniconfig==2.0.0", + "child2", ] + + [tool.uv.sources] + child2 = { workspace = true } "### ); }); + // `uv add` implies a full lock and sync, including development dependencies. let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; insta::with_settings!({ @@ -738,28 +659,27 @@ fn add_no_clean() -> Result<()> { requires-python = ">=3.12" [[distribution]] - name = "iniconfig" - version = "2.0.0" - source = "registry+https://pypi.org/simple" - sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } - wheels = [{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }] - - [[distribution]] - name = "project" + name = "child1" version = "0.1.0" - source = "editable+." - sdist = { path = "." } + source = "editable+child1" + sdist = { path = "child1" } [[distribution.dependencies]] - name = "iniconfig" - version = "2.0.0" - source = "registry+https://pypi.org/simple" + name = "child2" + version = "0.1.0" + source = "editable+child2" + + [[distribution]] + name = "child2" + version = "0.1.0" + source = "editable+child2" + sdist = { path = "child2" } "### ); }); - // Install from the lockfile without cleaning the environment. - uv_snapshot!(context.filters(), context.sync().arg("--no-clean"), @r###" + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###" success: true exit_code: 0 ----- stdout ----- @@ -769,26 +689,83 @@ fn add_no_clean() -> Result<()> { Audited 2 packages in [TIME] "###); - // Install from the lockfile, cleaning the environment. - uv_snapshot!(context.filters(), context.sync(), @r###" + // Remove the dependency. + uv_snapshot!(context.filters(), context.remove(&["child2"]).current_dir(&child1), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv remove` is experimental and may change without warning. + Resolved 2 packages in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 2 packages in [TIME] + Installed 1 package in [TIME] + - child1==0.1.0 (from file://[TEMP_DIR]/child1) + + child1==0.1.0 (from file://[TEMP_DIR]/child1) + - child2==0.1.0 (from file://[TEMP_DIR]/child2) + "###); + + let pyproject_toml = fs_err::read_to_string(child1.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv.sources] + "### + ); + }); + + 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 = "child1" + version = "0.1.0" + source = "editable+child1" + sdist = { path = "child1" } + + [[distribution]] + name = "child2" + version = "0.1.0" + source = "editable+child2" + sdist = { path = "child2" } + "### + ); + }); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- warning: `uv sync` is experimental and may change without warning. - Uninstalled 3 packages in [TIME] - - anyio==3.7.0 - - idna==3.6 - - sniffio==1.3.1 + Audited 1 package in [TIME] "###); Ok(()) } -/// Remove a PyPI requirement. +/// Update a PyPI requirement. #[test] -fn remove_registry() -> Result<()> { +fn update_registry() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -797,7 +774,10 @@ fn remove_registry() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] + dependencies = [ + "anyio == 3.7.0 ; python_version >= '3.12'", + "anyio < 3.7.0 ; python_version < '3.12'", + ] "#})?; uv_snapshot!(context.filters(), context.lock(), @r###" @@ -825,22 +805,21 @@ fn remove_registry() -> Result<()> { + sniffio==1.3.1 "###); - uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###" + uv_snapshot!(context.filters(), context.add(&["anyio==4.3.0"]), @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 4 packages in [TIME] - Installed 1 package in [TIME] + warning: `uv add` is experimental and may change without warning. + Resolved 4 packages in [TIME] + Downloaded 2 packages in [TIME] + Uninstalled 2 packages in [TIME] + Installed 2 packages in [TIME] - anyio==3.7.0 - - idna==3.6 + + anyio==4.3.0 - project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/) - - sniffio==1.3.1 "###); let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; @@ -854,7 +833,9 @@ fn remove_registry() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [] + dependencies = [ + "anyio==4.3.0", + ] "### ); }); @@ -869,11 +850,47 @@ fn remove_registry() -> Result<()> { version = 1 requires-python = ">=3.12" + [[distribution]] + name = "anyio" + version = "4.3.0" + source = "registry+https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }] + + [[distribution.dependencies]] + name = "idna" + version = "3.6" + source = "registry+https://pypi.org/simple" + + [[distribution.dependencies]] + name = "sniffio" + version = "1.3.1" + source = "registry+https://pypi.org/simple" + + [[distribution]] + name = "idna" + version = "3.6" + source = "registry+https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }] + [[distribution]] name = "project" version = "0.1.0" source = "editable+." sdist = { path = "." } + + [[distribution.dependencies]] + name = "anyio" + version = "4.3.0" + source = "registry+https://pypi.org/simple" + + [[distribution]] + name = "sniffio" + version = "1.3.1" + source = "registry+https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }] "### ); }); @@ -886,15 +903,15 @@ fn remove_registry() -> Result<()> { ----- stderr ----- warning: `uv sync` is experimental and may change without warning. - Audited 1 package in [TIME] + Audited 4 packages in [TIME] "###); Ok(()) } -/// Remove a development dependency. +/// Adding a dependency does not clean the environment. #[test] -fn remove_dev() -> Result<()> { +fn add_no_clean() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -903,10 +920,9 @@ fn remove_dev() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [] - - [tool.uv] - dev-dependencies = ["anyio==3.7.0"] + dependencies = [ + "anyio == 3.7.0", + ] "#})?; uv_snapshot!(context.filters(), context.lock(), @r###" @@ -934,33 +950,29 @@ fn remove_dev() -> Result<()> { + sniffio==1.3.1 "###); - uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - warning: `uv remove` is experimental and may change without warning. - warning: `anyio` is a development dependency; try calling `uv add --dev` - error: The dependency `anyio` could not be found in `dependencies` - "###); + // Manually remove a dependency. + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; - uv_snapshot!(context.filters(), context.remove(&["anyio"]).arg("--dev"), @r###" + uv_snapshot!(context.filters(), context.add(&["iniconfig==2.0.0"]), @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 4 packages in [TIME] - Installed 1 package in [TIME] - - anyio==3.7.0 - - idna==3.6 + warning: `uv add` is experimental and may change without warning. + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 - project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/) - - sniffio==1.3.1 "###); let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; @@ -974,10 +986,9 @@ fn remove_dev() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [] - - [tool.uv] - dev-dependencies = [] + dependencies = [ + "iniconfig==2.0.0", + ] "### ); }); @@ -992,16 +1003,39 @@ fn remove_dev() -> Result<()> { version = 1 requires-python = ">=3.12" + [[distribution]] + name = "iniconfig" + version = "2.0.0" + source = "registry+https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }] + [[distribution]] name = "project" version = "0.1.0" source = "editable+." sdist = { path = "." } + + [[distribution.dependencies]] + name = "iniconfig" + version = "2.0.0" + source = "registry+https://pypi.org/simple" "### ); }); - // Install from the lockfile. + // Install from the lockfile without cleaning the environment. + uv_snapshot!(context.filters(), context.sync().arg("--no-clean"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + Audited 2 packages in [TIME] + "###); + + // Install from the lockfile, cleaning the environment. uv_snapshot!(context.filters(), context.sync(), @r###" success: true exit_code: 0 @@ -1009,15 +1043,18 @@ fn remove_dev() -> Result<()> { ----- stderr ----- warning: `uv sync` is experimental and may change without warning. - Audited 1 package in [TIME] + Uninstalled 3 packages in [TIME] + - anyio==3.7.0 + - idna==3.6 + - sniffio==1.3.1 "###); Ok(()) } -/// Remove a PyPI requirement that occurs multiple times. +/// Remove a PyPI requirement. #[test] -fn remove_all_registry() -> Result<()> { +fn remove_registry() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -1026,10 +1063,7 @@ fn remove_all_registry() -> Result<()> { 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'", - ] + dependencies = ["anyio==3.7.0"] "#})?; uv_snapshot!(context.filters(), context.lock(), @r###"