Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort dependency group keys when adding new group #11591

Merged
merged 2 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/uv-workspace/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic we use for detecting sorted dependencies is a bit more complex. Specifically, we check whether the values are sorted case-insensitively or case-sensitively, and then respect that sort if so. We could do the same here, but I don't consider it blocking (it seems pretty rare for users to use capitalization in these identifiers, unlike in package names).


let group = dependency_groups
.entry(group.as_ref())
.or_insert(Item::Value(Value::Array(Array::new())))
Expand All @@ -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
Expand Down
109 changes: 109 additions & 0 deletions crates/uv/tests/it/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4826,6 +4826,112 @@ fn add_group() -> Result<()> {

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

insta::with_settings!({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a few test cases where there are comments between the groups, to understand how they move around?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 this is an unfortunately frequent cause of bug reports

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added two new tests

filters => context.filters(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random, but it doesn't actually look like you need the filters for this snapshot. You could reduce nesting by omitting them — but I don't mind either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed filters on these tests

}, {
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(),
}, {
Expand All @@ -4845,6 +4951,9 @@ fn add_group() -> Result<()> {
second = [
"anyio==3.7.0",
]
alpha = [
"anyio==3.7.0",
]
"###
);
});
Expand Down
Loading