Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 34 additions & 0 deletions crates/uv-workspace/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,8 +1009,12 @@ pub fn add_dependency(
enum Sort {
/// The list is sorted in a case-insensitive manner.
CaseInsensitive,
/// The list is sorted naively in a case-insensitive manner.
CaseInsensitiveNaive,
/// The list is sorted in a case-sensitive manner.
CaseSensitive,
/// The list is sorted naively in a case-sensitive manner.
CaseSensitiveNaive,
/// The list is unsorted.
Unsorted,
}
Expand All @@ -1029,13 +1033,25 @@ pub fn add_dependency(
)
}

/// Naively compare two [`Value`] requirements case-insensitively.
fn case_insensitive_naive(a: &Value, b: &Value) -> Ordering {
a.as_str()
.map(str::to_lowercase)
.cmp(&b.as_str().map(str::to_lowercase))
}

/// Compare two [`Value`] requirements case-sensitively.
fn case_sensitive(a: &Value, b: &Value) -> Ordering {
a.as_str()
.map(split_specifiers)
.cmp(&b.as_str().map(split_specifiers))
}

/// Naively compare two [`Value`] requirements case-sensitively.
fn case_sensitive_naive(a: &Value, b: &Value) -> Ordering {
a.as_str().cmp(&b.as_str())
}

// Determine if the dependency list is sorted prior to
// adding the new dependency; the new dependency list
// will be sorted only when the original list is sorted
Expand All @@ -1057,6 +1073,17 @@ pub fn add_dependency(
matches!(case_sensitive(a, b), Ordering::Less | Ordering::Equal)
}) {
Some(Sort::CaseSensitive)
} else if deps.iter().tuple_windows().all(|(a, b)| {
matches!(
case_insensitive_naive(a, b),
Ordering::Less | Ordering::Equal
)
}) {
Some(Sort::CaseInsensitiveNaive)
} else if deps.iter().tuple_windows().all(|(a, b)| {
matches!(case_sensitive_naive(a, b), Ordering::Less | Ordering::Equal)
}) {
Some(Sort::CaseSensitiveNaive)
} else {
None
}
Expand All @@ -1069,9 +1096,16 @@ pub fn add_dependency(
Sort::CaseInsensitive => deps.iter().position(|d| {
case_insensitive(d, &Value::from(req_string.as_str())) == Ordering::Greater
}),
Sort::CaseInsensitiveNaive => deps.iter().position(|d| {
case_insensitive_naive(d, &Value::from(req_string.as_str()))
== Ordering::Greater
}),
Sort::CaseSensitive => deps.iter().position(|d| {
case_sensitive(d, &Value::from(req_string.as_str())) == Ordering::Greater
}),
Sort::CaseSensitiveNaive => deps.iter().position(|d| {
case_sensitive_naive(d, &Value::from(req_string.as_str())) == Ordering::Greater
}),
Sort::Unsorted => None,
};
let index = index.unwrap_or(deps.len());
Expand Down
117 changes: 117 additions & 0 deletions crates/uv/tests/it/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7240,6 +7240,63 @@ fn sorted_dependencies() -> Result<()> {
Ok(())
}

/// Ensure that if the dependencies are sorted naively (i.e. by the whole
/// requirement specifier), that added dependencies are sorted in the same way.
#[test]
fn naive_sorted_dependencies() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();

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 = [
"pytest-mock>=3.14",
"pytest>=8.1.1",
]
"#})?;

uv_snapshot!(context.filters(), context.add().args(["pytest-randomly"]), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ iniconfig==2.0.0
+ packaging==24.0
+ pluggy==1.4.0
+ pytest==8.1.1
+ pytest-mock==3.14.0
+ pytest-randomly==3.15.0
");

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 = [
"pytest-mock>=3.14",
"pytest-randomly>=3.15.0",
"pytest>=8.1.1",
]
"###
);
});
Ok(())
}

/// Ensure that the added dependencies are case sensitive sorted if the dependency list was already
/// case sensitive sorted prior to the operation.
#[test]
Expand Down Expand Up @@ -7307,6 +7364,66 @@ fn case_sensitive_sorted_dependencies() -> Result<()> {
Ok(())
}

/// Ensure that if the dependencies are sorted naively (i.e. by the whole
/// requirement specifier), that added dependencies are sorted in the same way.
#[test]
fn case_sensitive_naive_sorted_dependencies() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();

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 = [
"Typing-extensions>=4.10.0",
"pytest-mock>=3.14",
"pytest>=8.1.1",
]
"#})?;

uv_snapshot!(context.filters(), context.add().args(["pytest-randomly"]), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ iniconfig==2.0.0
+ packaging==24.0
+ pluggy==1.4.0
+ pytest==8.1.1
+ pytest-mock==3.14.0
+ pytest-randomly==3.15.0
+ typing-extensions==4.10.0
");

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 = [
"Typing-extensions>=4.10.0",
"pytest-mock>=3.14",
"pytest-randomly>=3.15.0",
"pytest>=8.1.1",
]
"###
);
});
Ok(())
}

/// Ensure that sorting is based on the name, rather than the combined name-and-specifiers.
#[test]
fn sorted_dependencies_name_specifiers() -> Result<()> {
Expand Down
Loading