diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 9420934cf07bf..d219ff8fc4c40 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -43,6 +43,8 @@ pub struct PyProjectToml { pub project: Option, /// Tool-specific metadata. pub tool: Option, + /// Non-project dependency groups, as defined in PEP 735. + pub dependency_groups: Option>>, /// The raw unserialized document. #[serde(skip)] pub raw: String, @@ -1053,6 +1055,8 @@ pub enum DependencyType { Dev, /// A dependency in `project.optional-dependencies.{0}`. Optional(ExtraName), + /// A dependency in `project.dependency-groups.{0}`. + Group(ExtraName), } /// @@ -1081,3 +1085,30 @@ mod serde_from_and_to_string { .map_err(de::Error::custom) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use uv_pep508::ExtraName; + + use crate::pyproject::PyProjectToml; + + #[test] + fn test_read_dependency_groups() { + let toml = r#" +[dependency-groups] +test = ["a"] +"#; + + let result = + PyProjectToml::from_string(toml.to_string()).expect("Deserialization should succeed"); + let groups = result + .dependency_groups + .expect("`dependency-groups` should be present"); + let test = groups + .get(&ExtraName::from_str("test").unwrap()) + .expect("Group `test` should be present"); + assert_eq!(test, &["a".to_string()]); + } +} diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 75de5e001a4a3..2c39b5626ce59 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -495,6 +495,22 @@ impl PyProjectTomlMut { } } + // Check `tool.uv.dev-dependencies`. + if let Some(groups) = self.doc.get("dependency-groups").and_then(Item::as_table) { + for (group, dependencies) in groups { + let Some(dependencies) = dependencies.as_array() else { + continue; + }; + let Ok(group) = ExtraName::new(group.to_string()) else { + continue; + }; + + if !find_dependencies(name, marker, dependencies).is_empty() { + types.push(DependencyType::Group(group)); + } + } + } + // Check `tool.uv.dev-dependencies`. if let Some(dev_dependencies) = self .doc @@ -502,7 +518,7 @@ impl PyProjectTomlMut { .and_then(Item::as_table) .and_then(|tool| tool.get("uv")) .and_then(Item::as_table) - .and_then(|tool| tool.get("dev-dependencies")) + .and_then(|uv| uv.get("dev-dependencies")) .and_then(Item::as_array) { if !find_dependencies(name, marker, dev_dependencies).is_empty() { diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 43f57d48d0dea..994f9005cbb19 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -202,6 +202,7 @@ pub(crate) async fn add( bail!("Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead", "uv add --dev".green()) } DependencyType::Dev => (), + DependencyType::Group(_) => (), } } @@ -449,6 +450,7 @@ pub(crate) async fn add( DependencyType::Optional(ref group) => { toml.add_optional_dependency(group, &requirement, source.as_ref())? } + DependencyType::Group(_) => todo!("adding dependencies to groups is not yet supported"), }; // If the edit was inserted before the end of the list, update the existing edits. @@ -707,6 +709,9 @@ async fn lock_and_sync( DependencyType::Optional(ref group) => { toml.set_optional_dependency_minimum_version(group, *index, minimum)?; } + DependencyType::Group(_) => { + todo!("adding dependencies to groups is not yet supported") + } } modified = true; @@ -767,6 +772,7 @@ async fn lock_and_sync( let dev = DevMode::Exclude; (extras, dev) } + DependencyType::Group(_) => todo!("adding dependencies to groups is not yet supported"), }; project::sync::do_sync( diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index fd0155c7727d5..eca8c3ee2c9dc 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -120,6 +120,9 @@ pub(crate) async fn remove( ); } } + DependencyType::Group(_) => { + todo!("removing dependencies from groups is not yet supported") + } } } @@ -246,6 +249,9 @@ fn warn_if_present(name: &PackageName, pyproject: &PyProjectTomlMut) { "`{name}` is an optional dependency; try calling `uv remove --optional {group}`", ); } + DependencyType::Group(_) => { + // TODO(zanieb): Once we support `remove --group`, add a warning here. + } } } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 9f2474293b50b..bc961c2b27563 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -838,8 +838,8 @@ impl AddSettings { python, } = args; - let dependency_type = if let Some(group) = optional { - DependencyType::Optional(group) + let dependency_type = if let Some(extra) = optional { + DependencyType::Optional(extra) } else if dev { DependencyType::Dev } else {