Skip to content

Commit 4d134a4

Browse files
charliermarshzanieb
authored andcommitted
Error on duplicate PEP 735 dependency groups (#8390)
## Summary Part of: #8272.
1 parent 384f445 commit 4d134a4

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

crates/uv-workspace/src/pyproject.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub struct PyProjectToml {
4646
/// Tool-specific metadata.
4747
pub tool: Option<Tool>,
4848
/// Non-project dependency groups, as defined in PEP 735.
49-
pub dependency_groups: Option<BTreeMap<GroupName, Vec<DependencyGroupSpecifier>>>,
49+
pub dependency_groups: Option<DependencyGroups>,
5050
/// The raw unserialized document.
5151
#[serde(skip)]
5252
pub raw: String,
@@ -540,6 +540,79 @@ impl Deref for SerdePattern {
540540
}
541541
}
542542

543+
#[derive(Debug, Clone, PartialEq)]
544+
#[cfg_attr(test, derive(Serialize))]
545+
pub struct DependencyGroups(BTreeMap<GroupName, Vec<DependencyGroupSpecifier>>);
546+
547+
impl DependencyGroups {
548+
/// Returns the names of the dependency groups.
549+
pub fn keys(&self) -> impl Iterator<Item = &GroupName> {
550+
self.0.keys()
551+
}
552+
553+
/// Returns the dependency group with the given name.
554+
pub fn get(&self, group: &GroupName) -> Option<&Vec<DependencyGroupSpecifier>> {
555+
self.0.get(group)
556+
}
557+
558+
/// Returns an iterator over the dependency groups.
559+
pub fn iter(&self) -> impl Iterator<Item = (&GroupName, &Vec<DependencyGroupSpecifier>)> {
560+
self.0.iter()
561+
}
562+
}
563+
564+
impl<'a> IntoIterator for &'a DependencyGroups {
565+
type Item = (&'a GroupName, &'a Vec<DependencyGroupSpecifier>);
566+
type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec<DependencyGroupSpecifier>>;
567+
568+
fn into_iter(self) -> Self::IntoIter {
569+
self.0.iter()
570+
}
571+
}
572+
573+
/// Ensure that all keys in the TOML table are unique.
574+
impl<'de> serde::de::Deserialize<'de> for DependencyGroups {
575+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
576+
where
577+
D: Deserializer<'de>,
578+
{
579+
struct GroupVisitor;
580+
581+
impl<'de> serde::de::Visitor<'de> for GroupVisitor {
582+
type Value = DependencyGroups;
583+
584+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
585+
formatter.write_str("a table with unique dependency group names")
586+
}
587+
588+
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
589+
where
590+
M: serde::de::MapAccess<'de>,
591+
{
592+
let mut sources = BTreeMap::new();
593+
while let Some((key, value)) =
594+
access.next_entry::<GroupName, Vec<DependencyGroupSpecifier>>()?
595+
{
596+
match sources.entry(key) {
597+
std::collections::btree_map::Entry::Occupied(entry) => {
598+
return Err(serde::de::Error::custom(format!(
599+
"duplicate dependency group: `{}`",
600+
entry.key()
601+
)));
602+
}
603+
std::collections::btree_map::Entry::Vacant(entry) => {
604+
entry.insert(value);
605+
}
606+
}
607+
}
608+
Ok(DependencyGroups(sources))
609+
}
610+
}
611+
612+
deserializer.deserialize_map(GroupVisitor)
613+
}
614+
}
615+
543616
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
544617
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
545618
#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]

crates/uv/tests/it/lock.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16635,6 +16635,42 @@ fn lock_group_invalid_entry_group_name() -> Result<()> {
1663516635
Ok(())
1663616636
}
1663716637

16638+
#[test]
16639+
fn lock_group_invalid_duplicate_group_name() -> Result<()> {
16640+
let context = TestContext::new("3.12");
16641+
16642+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
16643+
pyproject_toml.write_str(
16644+
r#"
16645+
[project]
16646+
name = "project"
16647+
version = "0.1.0"
16648+
requires-python = ">=3.12"
16649+
dependencies = ["typing-extensions"]
16650+
16651+
[dependency-groups]
16652+
foo-bar = []
16653+
foo_bar = []
16654+
"#,
16655+
)?;
16656+
16657+
uv_snapshot!(context.filters(), context.lock(), @r###"
16658+
success: false
16659+
exit_code: 2
16660+
----- stdout -----
16661+
16662+
----- stderr -----
16663+
error: Failed to parse: `pyproject.toml`
16664+
Caused by: TOML parse error at line 8, column 9
16665+
|
16666+
8 | [dependency-groups]
16667+
| ^^^^^^^^^^^^^^^^^^^
16668+
duplicate dependency group: `foo-bar`
16669+
"###);
16670+
16671+
Ok(())
16672+
}
16673+
1663816674
#[test]
1663916675
fn lock_group_invalid_entry_table() -> Result<()> {
1664016676
let context = TestContext::new("3.12");

0 commit comments

Comments
 (0)