mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Validate dependency group includes (#19866)
## Summary Prior to this change, a dependency-group object containing `include-group` alongside additional keys was accepted as a group include. We silently discarded the additional fields even though the current specification defines an include object as containing exactly one key. This recognizes `include-group` only when it is the object's sole key. Mixed tables are preserved as dependency object specifiers, allowing future syntax to deserialize without being misinterpreted, while the existing semantic validation rejects them when their group is processed. The focused coverage verifies both that mixed tables retain all of their fields during deserialization and that `uv lock` rejects the currently unsupported object when resolving its group.
This commit is contained in:
@@ -129,12 +129,13 @@ impl<'de> Deserialize<'de> for DependencyGroupSpecifier {
|
||||
return Err(serde::de::Error::custom("missing field `include-group`"));
|
||||
}
|
||||
|
||||
if let Some(include_group) = map_data
|
||||
.get("include-group")
|
||||
.map(String::as_str)
|
||||
.map(GroupName::from_str)
|
||||
.transpose()
|
||||
.map_err(serde::de::Error::custom)?
|
||||
if map_data.len() == 1
|
||||
&& let Some(include_group) = map_data
|
||||
.get("include-group")
|
||||
.map(String::as_str)
|
||||
.map(GroupName::from_str)
|
||||
.transpose()
|
||||
.map_err(serde::de::Error::custom)?
|
||||
{
|
||||
Ok(DependencyGroupSpecifier::IncludeGroup { include_group })
|
||||
} else {
|
||||
|
||||
@@ -2178,6 +2178,7 @@ impl VirtualProject {
|
||||
#[cfg(test)]
|
||||
#[cfg(unix)] // Avoid path escaping for the unit tests
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
@@ -3244,6 +3245,7 @@ mod tests {
|
||||
[dependency-groups]
|
||||
foo = ["a", {include-group = "bar"}]
|
||||
bar = ["b"]
|
||||
future = [{include-group = "bar", unknown = "value"}]
|
||||
"#;
|
||||
|
||||
let result = PyProjectToml::from_string(toml.to_string(), "pyproject.toml")
|
||||
@@ -3272,6 +3274,17 @@ bar = ["b"]
|
||||
bar,
|
||||
&[DependencyGroupSpecifier::Requirement("b".to_string())]
|
||||
);
|
||||
|
||||
let future = groups
|
||||
.get(&GroupName::from_str("future").unwrap())
|
||||
.expect("Group `future` should be present");
|
||||
assert_eq!(
|
||||
future,
|
||||
&[DependencyGroupSpecifier::Object(BTreeMap::from([
|
||||
("include-group".to_string(), "bar".to_string()),
|
||||
("unknown".to_string(), "value".to_string()),
|
||||
]))]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -26231,6 +26231,38 @@ fn lock_group_invalid_entry_table() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_include_with_extra_key() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
foo = [{include-group = "bar", unknown = "value"}]
|
||||
bar = []
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Project `project` has malformed dependency groups
|
||||
Caused by: Group `foo` contains an unknown dependency object specifier: {"include-group": "bar", "unknown": "value"}
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_group_invalid_entry_type() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
Reference in New Issue
Block a user