Skip to content

Commit 4d2317c

Browse files
committed
Error on duplicate PEP 735 dependency groups (#8390)
## Summary Part of: #8272.
1 parent c375225 commit 4d2317c

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,
@@ -525,6 +525,79 @@ impl Deref for SerdePattern {
525525
}
526526
}
527527

528+
#[derive(Debug, Clone, PartialEq)]
529+
#[cfg_attr(test, derive(Serialize))]
530+
pub struct DependencyGroups(BTreeMap<GroupName, Vec<DependencyGroupSpecifier>>);
531+
532+
impl DependencyGroups {
533+
/// Returns the names of the dependency groups.
534+
pub fn keys(&self) -> impl Iterator<Item = &GroupName> {
535+
self.0.keys()
536+
}
537+
538+
/// Returns the dependency group with the given name.
539+
pub fn get(&self, group: &GroupName) -> Option<&Vec<DependencyGroupSpecifier>> {
540+
self.0.get(group)
541+
}
542+
543+
/// Returns an iterator over the dependency groups.
544+
pub fn iter(&self) -> impl Iterator<Item = (&GroupName, &Vec<DependencyGroupSpecifier>)> {
545+
self.0.iter()
546+
}
547+
}
548+
549+
impl<'a> IntoIterator for &'a DependencyGroups {
550+
type Item = (&'a GroupName, &'a Vec<DependencyGroupSpecifier>);
551+
type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec<DependencyGroupSpecifier>>;
552+
553+
fn into_iter(self) -> Self::IntoIter {
554+
self.0.iter()
555+
}
556+
}
557+
558+
/// Ensure that all keys in the TOML table are unique.
559+
impl<'de> serde::de::Deserialize<'de> for DependencyGroups {
560+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
561+
where
562+
D: Deserializer<'de>,
563+
{
564+
struct GroupVisitor;
565+
566+
impl<'de> serde::de::Visitor<'de> for GroupVisitor {
567+
type Value = DependencyGroups;
568+
569+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
570+
formatter.write_str("a table with unique dependency group names")
571+
}
572+
573+
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
574+
where
575+
M: serde::de::MapAccess<'de>,
576+
{
577+
let mut sources = BTreeMap::new();
578+
while let Some((key, value)) =
579+
access.next_entry::<GroupName, Vec<DependencyGroupSpecifier>>()?
580+
{
581+
match sources.entry(key) {
582+
std::collections::btree_map::Entry::Occupied(entry) => {
583+
return Err(serde::de::Error::custom(format!(
584+
"duplicate dependency group: `{}`",
585+
entry.key()
586+
)));
587+
}
588+
std::collections::btree_map::Entry::Vacant(entry) => {
589+
entry.insert(value);
590+
}
591+
}
592+
}
593+
Ok(DependencyGroups(sources))
594+
}
595+
}
596+
597+
deserializer.deserialize_map(GroupVisitor)
598+
}
599+
}
600+
528601
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
529602
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
530603
#[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
@@ -16409,6 +16409,42 @@ fn lock_group_invalid_entry_group_name() -> Result<()> {
1640916409
Ok(())
1641016410
}
1641116411

16412+
#[test]
16413+
fn lock_group_invalid_duplicate_group_name() -> Result<()> {
16414+
let context = TestContext::new("3.12");
16415+
16416+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
16417+
pyproject_toml.write_str(
16418+
r#"
16419+
[project]
16420+
name = "project"
16421+
version = "0.1.0"
16422+
requires-python = ">=3.12"
16423+
dependencies = ["typing-extensions"]
16424+
16425+
[dependency-groups]
16426+
foo-bar = []
16427+
foo_bar = []
16428+
"#,
16429+
)?;
16430+
16431+
uv_snapshot!(context.filters(), context.lock(), @r###"
16432+
success: false
16433+
exit_code: 2
16434+
----- stdout -----
16435+
16436+
----- stderr -----
16437+
error: Failed to parse: `pyproject.toml`
16438+
Caused by: TOML parse error at line 8, column 9
16439+
|
16440+
8 | [dependency-groups]
16441+
| ^^^^^^^^^^^^^^^^^^^
16442+
duplicate dependency group: `foo-bar`
16443+
"###);
16444+
16445+
Ok(())
16446+
}
16447+
1641216448
#[test]
1641316449
fn lock_group_invalid_entry_table() -> Result<()> {
1641416450
let context = TestContext::new("3.12");

0 commit comments

Comments
 (0)