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.
This commit is contained in:
John Mumm 2025-02-18 19:12:50 +01:00 committed by GitHub
parent 555bf89b38
commit b086437bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 256 additions and 21 deletions

View file

@ -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

View file

@ -4758,11 +4758,7 @@ fn add_group() -> Result<()> {
let pyproject_toml = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
assert_snapshot!(pyproject_toml, @r###"
[project]
name = "project"
version = "0.1.0"
@ -4774,8 +4770,7 @@ fn add_group() -> Result<()> {
"anyio==3.7.0",
]
"###
);
});
);
uv_snapshot!(context.filters(), context.add().arg("requests").arg("--group").arg("test"), @r###"
success: true
@ -4794,11 +4789,7 @@ fn add_group() -> Result<()> {
let pyproject_toml = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
assert_snapshot!(pyproject_toml, @r###"
[project]
name = "project"
version = "0.1.0"
@ -4811,8 +4802,7 @@ fn add_group() -> Result<()> {
"requests>=2.31.0",
]
"###
);
});
);
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("second"), @r###"
success: true
@ -4826,11 +4816,208 @@ fn add_group() -> Result<()> {
let pyproject_toml = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
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");
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 (sorted before the other groups).
#[test]
fn add_group_before_commented_groups() -> 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]
# This is our dev group
dev = [
"anyio==3.7.0",
]
# This is our test group
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]
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");
assert!(context.temp_dir.join("uv.lock").exists());
assert_snapshot!(pyproject_toml, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
alpha = [
"anyio==3.7.0",
]
# This is our dev group
dev = [
"anyio==3.7.0",
]
# This is our test group
test = [
"anyio==3.7.0",
"requests>=2.31.0",
]
"#
);
Ok(())
}
/// Add a requirement to dependency group (sorted between the other groups).
#[test]
fn add_group_between_commented_groups() -> 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]
# This is our dev group
dev = [
"anyio==3.7.0",
]
# This is our test group
test = [
"anyio==3.7.0",
"requests>=2.31.0",
]
"#})?;
uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0").arg("--group").arg("eta"), @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");
assert!(context.temp_dir.join("uv.lock").exists());
assert_snapshot!(pyproject_toml, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
# This is our dev group
dev = [
"anyio==3.7.0",
]
eta = [
"anyio==3.7.0",
]
# This is our test group
test = [
"anyio==3.7.0",
"requests>=2.31.0",
]
"#
);
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"
@ -4845,9 +5032,44 @@ fn add_group() -> Result<()> {
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");
assert_snapshot!(pyproject_toml, @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",
]
alpha = [
"anyio==3.7.0",
]
"###
);
});
);
assert!(context.temp_dir.join("uv.lock").exists());