diff --git a/crates/uv-distribution/src/pyproject_mut.rs b/crates/uv-distribution/src/pyproject_mut.rs index 917cf9d80e0c1..901e9db91aa53 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,25 +36,116 @@ 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(|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(|tool| tool.get_mut("uv")) + .and_then(|tool_uv| tool_uv.get_mut("sources")) + { + remove_source(req, sources)?; + } + + Ok(requirements) } /// Removes all occurrences of development dependencies with the given name. @@ -61,20 +154,36 @@ impl PyProjectTomlMut { 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") { + remove_source(req, sources)?; + }; + + 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 +193,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 +213,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) -> Result<(), Error> { + 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!(), + } + Ok(()) +} + +// Remove a source from `tool.uv.sources` by name. +fn remove_source(name: &PackageName, sources: &mut Item) -> Result<(), Error> { + if sources.is_none() { + return Ok(()); + } + + let sources = sources.as_table_mut().ok_or(Error::MalformedSources)?; + sources.remove(name.as_ref()); + Ok(()) +} + 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 a5f0819c82df7..6bdd38616f3da 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -1606,6 +1606,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 215c0a8d8a5cb..8e712cc5a41e5 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}; @@ -22,6 +23,7 @@ use crate::settings::ResolverInstallerSettings; #[allow(clippy::too_many_arguments)] pub(crate) async fn add( requirements: Vec, + source: Option, dev: bool, python: Option, settings: ResolverInstallerSettings, @@ -125,9 +127,9 @@ pub(crate) async fn add( let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?; for req in requirements.into_iter().map(pep508_rs::Requirement::from) { 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/main.rs b/crates/uv/src/main.rs index fe778e523b6a3..5829a7d0b97ff 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -693,6 +693,7 @@ async fn run() -> Result { commands::add( requirements, + args.source, args.dev, args.python, args.settings, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index fc90904fb5acd..b24a6a9803839 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -16,6 +16,7 @@ use uv_configuration::{ KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall, SetupPyStrategy, TargetTriple, Upgrade, }; +use uv_distribution::pyproject::Source; use uv_normalize::PackageName; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_settings::{ @@ -365,6 +366,7 @@ impl LockSettings { pub(crate) struct AddSettings { pub(crate) requirements: Vec, pub(crate) dev: bool, + pub(crate) source: Option, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, @@ -377,14 +379,25 @@ impl AddSettings { let AddArgs { requirements, dev, + workspace, installer, build, refresh, python, } = args; + let source = if workspace { + Some(Source::Workspace { + workspace: true, + editable: None, + }) + } else { + None + }; + Self { requirements, + source, dev, python, refresh: Refresh::from(refresh), diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 4b43a86867c2e..2f085d1d04177 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,64 +494,30 @@ 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 add --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] - - anyio==3.7.0 - + anyio==4.3.0 + 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]/) "###); @@ -567,9 +533,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 +551,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,67 +568,143 @@ 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(()) } -/// Remove a PyPI requirement. +/// Add and remove a workspace dependency. #[test] -fn remove_registry() -> 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("root/pyproject.toml"); + workspace.write_str(indoc! {r#" + [tool.uv.workspace] + members = ["child1", "child2"] + "#})?; + + let pyproject_toml = context.temp_dir.child("root/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###" + let pyproject_toml = context.temp_dir.child("root/child2/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "child2" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + let child1 = context.temp_dir.join("root/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 lock` is experimental and may change without warning. - Resolved 4 packages in [TIME] + Using Python 3.12.[X] interpreter at: /home/ibraheem/.pyenv/versions/3.12.[X]/bin/python3 + Creating virtualenv at: [TEMP_DIR]/root/[VENV]/ + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + child1==0.1.0 (from file://[TEMP_DIR]/root/child1) + + child2==0.1.0 (from file://[TEMP_DIR]/root/child2) "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + 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 = [ + "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("root/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.dependencies]] + 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. + 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. - 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 + Audited 2 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.remove(&["anyio"]), @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 1 package in [TIME] + Resolved 2 packages 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]/) + - child1==0.1.0 (from file://[TEMP_DIR]/root/child1) + + child1==0.1.0 (from file://[TEMP_DIR]/root/child1) "###); - 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(), @@ -705,15 +712,17 @@ fn remove_registry() -> Result<()> { assert_snapshot!( pyproject_toml, @r###" [project] - name = "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"))?; + let lock = fs_err::read_to_string(context.temp_dir.join("root/uv.lock"))?; insta::with_settings!({ filters => context.filters(), @@ -724,16 +733,22 @@ fn remove_registry() -> Result<()> { requires-python = ">=3.12" [[distribution]] - name = "project" + name = "child1" version = "0.1.0" - source = "editable+." - sdist = { path = "." } + 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(), @r###" + uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###" success: true exit_code: 0 ----- stdout ----- @@ -746,9 +761,9 @@ fn remove_registry() -> Result<()> { Ok(()) } -/// Remove a development dependency. +/// Update a PyPI requirement. #[test] -fn remove_dev() -> Result<()> { +fn update_registry() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -757,10 +772,10 @@ 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 ; python_version >= '3.12'", + "anyio < 3.7.0 ; python_version < '3.12'", + ] "#})?; uv_snapshot!(context.filters(), context.lock(), @r###" @@ -788,28 +803,19 @@ 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` - "###); - - uv_snapshot!(context.filters(), context.remove(&["anyio"]).arg("--dev"), @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 1 package 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 + + anyio==4.3.0 - project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/) "###); @@ -825,10 +831,9 @@ fn remove_dev() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = [] - - [tool.uv] - dev-dependencies = [] + dependencies = [ + "anyio==4.3.0", + ] "### ); }); @@ -843,11 +848,47 @@ fn remove_dev() -> 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 }] "### ); }); @@ -860,15 +901,15 @@ fn remove_dev() -> 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 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"); @@ -877,10 +918,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###"