Skip to content

Commit b086437

Browse files
authored
Sort dependency group keys when adding new group (#11591)
This change keeps dependency group keys sorted when adding new ones. If earlier dependency group keys were not sorted, we just append the new group key to avoid churn in `pyproject.toml`. See discussion on #11447. I've added a new snapshot test to capture this case. Closes #11447.
1 parent 555bf89 commit b086437

File tree

2 files changed

+256
-21
lines changed

2 files changed

+256
-21
lines changed

crates/uv-workspace/src/pyproject_mut.rs

+13
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,13 @@ impl PyProjectTomlMut {
450450
.as_table_like_mut()
451451
.ok_or(Error::MalformedDependencies)?;
452452

453+
let was_sorted = dependency_groups
454+
.get_values()
455+
.iter()
456+
.filter_map(|(dotted_ks, _)| dotted_ks.first())
457+
.map(|k| k.get())
458+
.is_sorted();
459+
453460
let group = dependency_groups
454461
.entry(group.as_ref())
455462
.or_insert(Item::Value(Value::Array(Array::new())))
@@ -459,6 +466,12 @@ impl PyProjectTomlMut {
459466
let name = req.name.clone();
460467
let added = add_dependency(req, group, source.is_some())?;
461468

469+
// To avoid churn in pyproject.toml, we only sort new group keys if the
470+
// existing keys were sorted.
471+
if was_sorted {
472+
dependency_groups.sort_values();
473+
}
474+
462475
// If `dependency-groups` is an inline table, reformat it.
463476
//
464477
// Reformatting can drop comments between keys, but you can't put comments

crates/uv/tests/it/edit.rs

+243-21
Original file line numberDiff line numberDiff line change
@@ -4758,11 +4758,7 @@ fn add_group() -> Result<()> {
47584758

47594759
let pyproject_toml = context.read("pyproject.toml");
47604760

4761-
insta::with_settings!({
4762-
filters => context.filters(),
4763-
}, {
4764-
assert_snapshot!(
4765-
pyproject_toml, @r###"
4761+
assert_snapshot!(pyproject_toml, @r###"
47664762
[project]
47674763
name = "project"
47684764
version = "0.1.0"
@@ -4774,8 +4770,7 @@ fn add_group() -> Result<()> {
47744770
"anyio==3.7.0",
47754771
]
47764772
"###
4777-
);
4778-
});
4773+
);
47794774

47804775
uv_snapshot!(context.filters(), context.add().arg("requests").arg("--group").arg("test"), @r###"
47814776
success: true
@@ -4794,11 +4789,7 @@ fn add_group() -> Result<()> {
47944789

47954790
let pyproject_toml = context.read("pyproject.toml");
47964791

4797-
insta::with_settings!({
4798-
filters => context.filters(),
4799-
}, {
4800-
assert_snapshot!(
4801-
pyproject_toml, @r###"
4792+
assert_snapshot!(pyproject_toml, @r###"
48024793
[project]
48034794
name = "project"
48044795
version = "0.1.0"
@@ -4811,8 +4802,7 @@ fn add_group() -> Result<()> {
48114802
"requests>=2.31.0",
48124803
]
48134804
"###
4814-
);
4815-
});
4805+
);
48164806

48174807
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("second"), @r###"
48184808
success: true
@@ -4826,11 +4816,241 @@ fn add_group() -> Result<()> {
48264816

48274817
let pyproject_toml = context.read("pyproject.toml");
48284818

4829-
insta::with_settings!({
4830-
filters => context.filters(),
4831-
}, {
4832-
assert_snapshot!(
4833-
pyproject_toml, @r###"
4819+
assert_snapshot!(pyproject_toml, @r#"
4820+
[project]
4821+
name = "project"
4822+
version = "0.1.0"
4823+
requires-python = ">=3.12"
4824+
dependencies = []
4825+
4826+
[dependency-groups]
4827+
second = [
4828+
"anyio==3.7.0",
4829+
]
4830+
test = [
4831+
"anyio==3.7.0",
4832+
"requests>=2.31.0",
4833+
]
4834+
"#
4835+
);
4836+
4837+
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("alpha"), @r###"
4838+
success: true
4839+
exit_code: 0
4840+
----- stdout -----
4841+
4842+
----- stderr -----
4843+
Resolved 8 packages in [TIME]
4844+
Audited 3 packages in [TIME]
4845+
"###);
4846+
4847+
let pyproject_toml = context.read("pyproject.toml");
4848+
4849+
assert_snapshot!(pyproject_toml, @r#"
4850+
[project]
4851+
name = "project"
4852+
version = "0.1.0"
4853+
requires-python = ">=3.12"
4854+
dependencies = []
4855+
4856+
[dependency-groups]
4857+
alpha = [
4858+
"anyio==3.7.0",
4859+
]
4860+
second = [
4861+
"anyio==3.7.0",
4862+
]
4863+
test = [
4864+
"anyio==3.7.0",
4865+
"requests>=2.31.0",
4866+
]
4867+
"#
4868+
);
4869+
4870+
assert!(context.temp_dir.join("uv.lock").exists());
4871+
4872+
Ok(())
4873+
}
4874+
4875+
/// Add a requirement to a dependency group (sorted before the other groups).
4876+
#[test]
4877+
fn add_group_before_commented_groups() -> Result<()> {
4878+
let context = TestContext::new("3.12");
4879+
4880+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
4881+
pyproject_toml.write_str(indoc! {r#"
4882+
[project]
4883+
name = "project"
4884+
version = "0.1.0"
4885+
requires-python = ">=3.12"
4886+
dependencies = []
4887+
4888+
[dependency-groups]
4889+
# This is our dev group
4890+
dev = [
4891+
"anyio==3.7.0",
4892+
]
4893+
# This is our test group
4894+
test = [
4895+
"anyio==3.7.0",
4896+
"requests>=2.31.0",
4897+
]
4898+
"#})?;
4899+
4900+
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("alpha"), @r"
4901+
success: true
4902+
exit_code: 0
4903+
----- stdout -----
4904+
4905+
----- stderr -----
4906+
Resolved 8 packages in [TIME]
4907+
Prepared 3 packages in [TIME]
4908+
Installed 3 packages in [TIME]
4909+
+ anyio==3.7.0
4910+
+ idna==3.6
4911+
+ sniffio==1.3.1
4912+
");
4913+
4914+
let pyproject_toml = context.read("pyproject.toml");
4915+
4916+
assert!(context.temp_dir.join("uv.lock").exists());
4917+
4918+
assert_snapshot!(pyproject_toml, @r#"
4919+
[project]
4920+
name = "project"
4921+
version = "0.1.0"
4922+
requires-python = ">=3.12"
4923+
dependencies = []
4924+
4925+
[dependency-groups]
4926+
alpha = [
4927+
"anyio==3.7.0",
4928+
]
4929+
# This is our dev group
4930+
dev = [
4931+
"anyio==3.7.0",
4932+
]
4933+
# This is our test group
4934+
test = [
4935+
"anyio==3.7.0",
4936+
"requests>=2.31.0",
4937+
]
4938+
"#
4939+
);
4940+
4941+
Ok(())
4942+
}
4943+
4944+
/// Add a requirement to dependency group (sorted between the other groups).
4945+
#[test]
4946+
fn add_group_between_commented_groups() -> Result<()> {
4947+
let context = TestContext::new("3.12");
4948+
4949+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
4950+
pyproject_toml.write_str(indoc! {r#"
4951+
[project]
4952+
name = "project"
4953+
version = "0.1.0"
4954+
requires-python = ">=3.12"
4955+
dependencies = []
4956+
4957+
[dependency-groups]
4958+
# This is our dev group
4959+
dev = [
4960+
"anyio==3.7.0",
4961+
]
4962+
# This is our test group
4963+
test = [
4964+
"anyio==3.7.0",
4965+
"requests>=2.31.0",
4966+
]
4967+
"#})?;
4968+
4969+
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("eta"), @r"
4970+
success: true
4971+
exit_code: 0
4972+
----- stdout -----
4973+
4974+
----- stderr -----
4975+
Resolved 8 packages in [TIME]
4976+
Prepared 3 packages in [TIME]
4977+
Installed 3 packages in [TIME]
4978+
+ anyio==3.7.0
4979+
+ idna==3.6
4980+
+ sniffio==1.3.1
4981+
");
4982+
4983+
let pyproject_toml = context.read("pyproject.toml");
4984+
4985+
assert!(context.temp_dir.join("uv.lock").exists());
4986+
4987+
assert_snapshot!(pyproject_toml, @r#"
4988+
[project]
4989+
name = "project"
4990+
version = "0.1.0"
4991+
requires-python = ">=3.12"
4992+
dependencies = []
4993+
4994+
[dependency-groups]
4995+
# This is our dev group
4996+
dev = [
4997+
"anyio==3.7.0",
4998+
]
4999+
eta = [
5000+
"anyio==3.7.0",
5001+
]
5002+
# This is our test group
5003+
test = [
5004+
"anyio==3.7.0",
5005+
"requests>=2.31.0",
5006+
]
5007+
"#
5008+
);
5009+
5010+
Ok(())
5011+
}
5012+
5013+
/// Add a requirement to a dependency group when existing dependency group
5014+
/// keys are not sorted.
5015+
#[test]
5016+
fn add_group_to_unsorted() -> Result<()> {
5017+
let context = TestContext::new("3.12");
5018+
5019+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
5020+
pyproject_toml.write_str(indoc! {r#"
5021+
[project]
5022+
name = "project"
5023+
version = "0.1.0"
5024+
requires-python = ">=3.12"
5025+
dependencies = []
5026+
5027+
[dependency-groups]
5028+
test = [
5029+
"anyio==3.7.0",
5030+
"requests>=2.31.0",
5031+
]
5032+
second = [
5033+
"anyio==3.7.0",
5034+
]
5035+
"#})?;
5036+
5037+
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("alpha"), @r###"
5038+
success: true
5039+
exit_code: 0
5040+
----- stdout -----
5041+
5042+
----- stderr -----
5043+
Resolved 8 packages in [TIME]
5044+
Prepared 3 packages in [TIME]
5045+
Installed 3 packages in [TIME]
5046+
+ anyio==3.7.0
5047+
+ idna==3.6
5048+
+ sniffio==1.3.1
5049+
"###);
5050+
5051+
let pyproject_toml = context.read("pyproject.toml");
5052+
5053+
assert_snapshot!(pyproject_toml, @r###"
48345054
[project]
48355055
name = "project"
48365056
version = "0.1.0"
@@ -4845,9 +5065,11 @@ fn add_group() -> Result<()> {
48455065
second = [
48465066
"anyio==3.7.0",
48475067
]
5068+
alpha = [
5069+
"anyio==3.7.0",
5070+
]
48485071
"###
4849-
);
4850-
});
5072+
);
48515073

48525074
assert!(context.temp_dir.join("uv.lock").exists());
48535075

0 commit comments

Comments
 (0)