diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 2b11f5503ffb..bc2526c2835b 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -450,6 +450,13 @@ impl PyProjectTomlMut { .as_table_like_mut() .ok_or(Error::MalformedDependencies)?; + let was_sorted = dependency_groups + .get_values() + .iter() + .filter_map(|(dotted_ks, _)| dotted_ks.first()) + .map(|k| k.get()) + .is_sorted(); + let group = dependency_groups .entry(group.as_ref()) .or_insert(Item::Value(Value::Array(Array::new()))) @@ -459,6 +466,12 @@ impl PyProjectTomlMut { let name = req.name.clone(); let added = add_dependency(req, group, source.is_some())?; + // To avoid churn in pyproject.toml, we only sort new group keys if the + // existing keys were sorted. + if was_sorted { + dependency_groups.sort_values(); + } + // If `dependency-groups` is an inline table, reformat it. // // Reformatting can drop comments between keys, but you can't put comments diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 7411ee2f39cc..49f1582f100a 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -4826,6 +4826,112 @@ fn add_group() -> Result<()> { let pyproject_toml = context.read("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 = [] + + [dependency-groups] + second = [ + "anyio==3.7.0", + ] + test = [ + "anyio==3.7.0", + "requests>=2.31.0", + ] + "# + ); + }); + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("alpha"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Audited 3 packages in [TIME] + "###); + + let pyproject_toml = context.read("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 = [] + + [dependency-groups] + alpha = [ + "anyio==3.7.0", + ] + second = [ + "anyio==3.7.0", + ] + test = [ + "anyio==3.7.0", + "requests>=2.31.0", + ] + "# + ); + }); + + assert!(context.temp_dir.join("uv.lock").exists()); + + Ok(()) +} + +/// Add a requirement to a dependency group when existing dependency group +/// keys are not sorted. +#[test] +fn add_group_to_unsorted() -> 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 = [] + + [dependency-groups] + test = [ + "anyio==3.7.0", + "requests>=2.31.0", + ] + second = [ + "anyio==3.7.0", + ] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("alpha"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + insta::with_settings!({ filters => context.filters(), }, { @@ -4845,6 +4951,9 @@ fn add_group() -> Result<()> { second = [ "anyio==3.7.0", ] + alpha = [ + "anyio==3.7.0", + ] "### ); });