From eefc8c6d3cfb4fe6c8df35781973ba670d767c0e Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Tue, 18 Jun 2024 12:26:07 -0400 Subject: [PATCH] Add `--workspace` option to `uv add` (#4362) ## Summary Implements `uv add foo --workspace`, which adds `foo` as a workspace dependency with the corresponding `tool.uv.sources` entry. Part of https://github.com/astral-sh/uv/issues/3959. --- crates/uv-distribution/src/pyproject_mut.rs | 194 ++++++-- crates/uv/src/cli.rs | 4 + crates/uv/src/commands/project/add.rs | 17 +- crates/uv/src/commands/project/remove.rs | 4 +- crates/uv/src/main.rs | 9 +- crates/uv/src/settings.rs | 11 +- crates/uv/tests/edit.rs | 502 +++++++++++--------- 7 files changed, 462 insertions(+), 279 deletions(-) diff --git a/crates/uv-distribution/src/pyproject_mut.rs b/crates/uv-distribution/src/pyproject_mut.rs index 917cf9d80..e35c82aa0 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())); + 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)?; - add_dependency(req, &mut tool_uv["dev-dependencies"]) + // 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 309a1a8db..1f26792ac 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 eae2a395b..62c14afdc 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) { - if dev { - pyproject.add_dev_dependency(&req)?; + let source = if workspace { + Some(Source::Workspace { + workspace: true, + editable: None, + }) } else { - pyproject.add_dependency(&req)?; + None + }; + + if dev { + pyproject.add_dev_dependency(&req, source.as_ref())?; + } else { + 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 b7808da2a..da097495a 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 20b40fe1e..ea0f53f63 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 af5f5bf42..abf9449b9 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 92c91d137..3f899a555 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,6 +494,272 @@ fn add_dev() -> Result<()> { Audited 4 packages in [TIME] "###); + // This should fail without --dev. + 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 remove --dev` + error: The dependency `anyio` could not be found in `dependencies` + "###); + + // Remove the dependency. + uv_snapshot!(context.filters(), context.remove(&["anyio"]).arg("--dev"), @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.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"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-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+." + sdist = { path = "." } + "### + ); + }); + + // 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(()) +} + +/// Add and remove a workspace dependency. +#[test] +fn add_remove_workspace() -> Result<()> { + let context = TestContext::new("3.12"); + + 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 = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + let pyproject_toml = context.temp_dir.child("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("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 ----- + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + 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 = [ + "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!({ + 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. + Audited 2 packages in [TIME] + "###); + + // 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. + Audited 1 package in [TIME] + "###); + Ok(()) } @@ -891,235 +1157,3 @@ fn remove_registry() -> Result<()> { Ok(()) } - -/// Remove a development dependency. -#[test] -fn remove_dev() -> 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 = [] - - [tool.uv] - dev-dependencies = ["anyio==3.7.0"] - "#})?; - - 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 - "###); - - 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###" - 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 - - 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"))?; - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - pyproject_toml, @r###" - [project] - name = "project" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = [] - - [tool.uv] - dev-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+." - sdist = { path = "." } - "### - ); - }); - - // 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(()) -} - -/// 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 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 - "###); - - 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 4 packages in [TIME] - Installed 1 package in [TIME] - - anyio==3.7.0 - - idna==3.6 - - 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"))?; - - 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+." - sdist = { path = "." } - "### - ); - }); - - // 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(()) -}