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:
Charlie Marsh
2026-06-15 18:08:04 -04:00
committed by GitHub
parent 3b1e14ea9a
commit d261212ca6
3 changed files with 52 additions and 6 deletions
@@ -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 {
+13
View File
@@ -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]
+32
View File
@@ -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");