diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 99d7cebde468..4b5351a9765e 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -44,6 +44,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, @@ -1141,6 +1143,8 @@ pub enum DependencyType { Dev, /// A dependency in `project.optional-dependencies.{0}`. Optional(ExtraName), + /// A dependency in `dependency-groups.{0}`. + Group(ExtraName), } /// diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 76ff3da25ca1..719ddd02e75f 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -712,6 +712,22 @@ impl PyProjectTomlMut { } } + // Check `dependency-groups`. + 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 @@ -719,7 +735,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-workspace/src/workspace/tests.rs b/crates/uv-workspace/src/workspace/tests.rs index 82978abdea7b..294c54293197 100644 --- a/crates/uv-workspace/src/workspace/tests.rs +++ b/crates/uv-workspace/src/workspace/tests.rs @@ -1,12 +1,15 @@ use std::env; - use std::path::Path; +use std::str::FromStr; use anyhow::Result; use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; use insta::assert_json_snapshot; +use uv_pep508::ExtraName; + +use crate::pyproject::PyProjectToml; use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { @@ -76,7 +79,8 @@ async fn albatross_in_example() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } @@ -128,7 +132,8 @@ async fn albatross_project_in_excluded() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } @@ -237,7 +242,8 @@ async fn albatross_root_workspace() { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } @@ -326,7 +332,8 @@ async fn albatross_virtual_workspace() { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } @@ -377,7 +384,8 @@ async fn albatross_just_project() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } @@ -528,7 +536,8 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } @@ -629,7 +638,8 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } @@ -743,7 +753,8 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } @@ -831,7 +842,8 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } @@ -840,3 +852,21 @@ async fn exclude_package() -> Result<()> { Ok(()) } + +#[test] +fn 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/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index e5aba53b102d..46883e9575f3 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -204,6 +204,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(_) => (), } } @@ -469,6 +470,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. @@ -743,6 +745,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; @@ -817,6 +822,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 4d6bf27ac18c..15d75ab59d37 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -122,6 +122,9 @@ pub(crate) async fn remove( ); } } + DependencyType::Group(_) => { + todo!("removing dependencies from groups is not yet supported") + } } } @@ -250,6 +253,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 d9bde2a2a030..3a526d5806c9 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -849,8 +849,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 {