Error on duplicate PEP 735 dependency groups (#8390)

## Summary

Part of: https://github.com/astral-sh/uv/pull/8272.
This commit is contained in:
Charlie Marsh 2024-10-21 21:47:57 -04:00 committed by Zanie Blue
parent 384f4459a1
commit 4d134a4ffe
2 changed files with 110 additions and 1 deletions

View File

@ -46,7 +46,7 @@ pub struct PyProjectToml {
/// Tool-specific metadata. /// Tool-specific metadata.
pub tool: Option<Tool>, pub tool: Option<Tool>,
/// Non-project dependency groups, as defined in PEP 735. /// Non-project dependency groups, as defined in PEP 735.
pub dependency_groups: Option<BTreeMap<GroupName, Vec<DependencyGroupSpecifier>>>, pub dependency_groups: Option<DependencyGroups>,
/// The raw unserialized document. /// The raw unserialized document.
#[serde(skip)] #[serde(skip)]
pub raw: String, pub raw: String,
@ -540,6 +540,79 @@ impl Deref for SerdePattern {
} }
} }
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(test, derive(Serialize))]
pub struct DependencyGroups(BTreeMap<GroupName, Vec<DependencyGroupSpecifier>>);
impl DependencyGroups {
/// Returns the names of the dependency groups.
pub fn keys(&self) -> impl Iterator<Item = &GroupName> {
self.0.keys()
}
/// Returns the dependency group with the given name.
pub fn get(&self, group: &GroupName) -> Option<&Vec<DependencyGroupSpecifier>> {
self.0.get(group)
}
/// Returns an iterator over the dependency groups.
pub fn iter(&self) -> impl Iterator<Item = (&GroupName, &Vec<DependencyGroupSpecifier>)> {
self.0.iter()
}
}
impl<'a> IntoIterator for &'a DependencyGroups {
type Item = (&'a GroupName, &'a Vec<DependencyGroupSpecifier>);
type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec<DependencyGroupSpecifier>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
/// Ensure that all keys in the TOML table are unique.
impl<'de> serde::de::Deserialize<'de> for DependencyGroups {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct GroupVisitor;
impl<'de> serde::de::Visitor<'de> for GroupVisitor {
type Value = DependencyGroups;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a table with unique dependency group names")
}
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut sources = BTreeMap::new();
while let Some((key, value)) =
access.next_entry::<GroupName, Vec<DependencyGroupSpecifier>>()?
{
match sources.entry(key) {
std::collections::btree_map::Entry::Occupied(entry) => {
return Err(serde::de::Error::custom(format!(
"duplicate dependency group: `{}`",
entry.key()
)));
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
Ok(DependencyGroups(sources))
}
}
deserializer.deserialize_map(GroupVisitor)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", try_from = "SourcesWire")] #[serde(rename_all = "kebab-case", try_from = "SourcesWire")]

View File

@ -16635,6 +16635,42 @@ fn lock_group_invalid_entry_group_name() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn lock_group_invalid_duplicate_group_name() -> Result<()> {
let context = TestContext::new("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 = ["typing-extensions"]
[dependency-groups]
foo-bar = []
foo_bar = []
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 8, column 9
|
8 | [dependency-groups]
| ^^^^^^^^^^^^^^^^^^^
duplicate dependency group: `foo-bar`
"###);
Ok(())
}
#[test] #[test]
fn lock_group_invalid_entry_table() -> Result<()> { fn lock_group_invalid_entry_table() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");