diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 9420934cf07b..430775d41456 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 `dependency-groups.{0}`. + Group(ExtraName), } /// diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 75de5e001a4a..496df0310062 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -495,6 +495,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 @@ -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-workspace/src/workspace/tests.rs b/crates/uv-workspace/src/workspace/tests.rs index f12d130ab100..67fa8f8c8f67 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) { @@ -43,7 +46,7 @@ async fn albatross_in_example() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-in-example/examples/bird-feeder", "project_name": "bird-feeder", @@ -75,11 +78,12 @@ async fn albatross_in_example() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } - "#); + "###); }); } @@ -94,7 +98,7 @@ async fn albatross_project_in_excluded() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", "project_name": "bird-feeder", @@ -126,11 +130,12 @@ async fn albatross_project_in_excluded() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } - "#); + "###); }); } @@ -144,7 +149,7 @@ async fn albatross_root_workspace() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-root-workspace", "project_name": "albatross", @@ -233,11 +238,12 @@ async fn albatross_root_workspace() { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } - "#); + "###); }); } @@ -252,7 +258,7 @@ async fn albatross_virtual_workspace() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-virtual-workspace/packages/albatross", "project_name": "albatross", @@ -320,11 +326,12 @@ async fn albatross_virtual_workspace() { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } - "#); + "###); }); } @@ -338,7 +345,7 @@ async fn albatross_just_project() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-just-project", "project_name": "albatross", @@ -370,11 +377,12 @@ async fn albatross_just_project() { ], "optional-dependencies": null }, - "tool": null + "tool": null, + "dependency-groups": null } } } - "#); + "###); }); } #[tokio::test] @@ -456,7 +464,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -519,11 +527,12 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } - "#); + "###); }); // Rewrite the members to both include and exclude `bird-feeder` by name. @@ -554,7 +563,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -618,11 +627,12 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } - "#); + "###); }); // Rewrite the exclusion to use the top-level directory (`packages`). @@ -653,7 +663,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -730,11 +740,12 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } - "#); + "###); }); // Rewrite the exclusion to use the top-level directory with a glob (`packages/*`). @@ -765,7 +776,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -816,12 +827,31 @@ async fn exclude_package() -> Result<()> { "override-dependencies": null, "constraint-dependencies": null } - } + }, + "dependency-groups": null } } } - "#); + "###); }); 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 e69916d5bf1c..785ab3cf1476 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(_) => (), } } @@ -451,6 +452,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. @@ -710,6 +712,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; @@ -779,6 +784,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 1e39f88b8646..98b568aeedfc 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 7de3dae71410..0a0475939ff8 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 {