Skip to content

Commit 43ae09a

Browse files
committed
Add support for reading PEP 735 dependency groups (#8104)
Part of #8090 As a basic first step, we parse these groups defined in `pyproject.toml` files.
1 parent c162078 commit 43ae09a

File tree

6 files changed

+75
-13
lines changed

6 files changed

+75
-13
lines changed

crates/uv-workspace/src/pyproject.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub struct PyProjectToml {
4444
pub project: Option<Project>,
4545
/// Tool-specific metadata.
4646
pub tool: Option<Tool>,
47+
/// Non-project dependency groups, as defined in PEP 735.
48+
pub dependency_groups: Option<BTreeMap<ExtraName, Vec<String>>>,
4749
/// The raw unserialized document.
4850
#[serde(skip)]
4951
pub raw: String,
@@ -1126,6 +1128,8 @@ pub enum DependencyType {
11261128
Dev,
11271129
/// A dependency in `project.optional-dependencies.{0}`.
11281130
Optional(ExtraName),
1131+
/// A dependency in `dependency-groups.{0}`.
1132+
Group(ExtraName),
11291133
}
11301134

11311135
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>

crates/uv-workspace/src/pyproject_mut.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,30 @@ impl PyProjectTomlMut {
578578
}
579579
}
580580

581+
// Check `dependency-groups`.
582+
if let Some(groups) = self.doc.get("dependency-groups").and_then(Item::as_table) {
583+
for (group, dependencies) in groups {
584+
let Some(dependencies) = dependencies.as_array() else {
585+
continue;
586+
};
587+
let Ok(group) = ExtraName::new(group.to_string()) else {
588+
continue;
589+
};
590+
591+
if !find_dependencies(name, marker, dependencies).is_empty() {
592+
types.push(DependencyType::Group(group));
593+
}
594+
}
595+
}
596+
581597
// Check `tool.uv.dev-dependencies`.
582598
if let Some(dev_dependencies) = self
583599
.doc
584600
.get("tool")
585601
.and_then(Item::as_table)
586602
.and_then(|tool| tool.get("uv"))
587603
.and_then(Item::as_table)
588-
.and_then(|tool| tool.get("dev-dependencies"))
604+
.and_then(|uv| uv.get("dev-dependencies"))
589605
.and_then(Item::as_array)
590606
{
591607
if !find_dependencies(name, marker, dev_dependencies).is_empty() {

crates/uv-workspace/src/workspace/tests.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use std::env;
2-
32
use std::path::Path;
3+
use std::str::FromStr;
44

55
use anyhow::Result;
66
use assert_fs::fixture::ChildPath;
77
use assert_fs::prelude::*;
88
use insta::assert_json_snapshot;
99

10+
use uv_pep508::ExtraName;
11+
12+
use crate::pyproject::PyProjectToml;
1013
use crate::workspace::{DiscoveryOptions, ProjectWorkspace};
1114

1215
async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) {
@@ -76,7 +79,8 @@ async fn albatross_in_example() {
7679
],
7780
"optional-dependencies": null
7881
},
79-
"tool": null
82+
"tool": null,
83+
"dependency-groups": null
8084
}
8185
}
8286
}
@@ -128,7 +132,8 @@ async fn albatross_project_in_excluded() {
128132
],
129133
"optional-dependencies": null
130134
},
131-
"tool": null
135+
"tool": null,
136+
"dependency-groups": null
132137
}
133138
}
134139
}
@@ -237,7 +242,8 @@ async fn albatross_root_workspace() {
237242
"override-dependencies": null,
238243
"constraint-dependencies": null
239244
}
240-
}
245+
},
246+
"dependency-groups": null
241247
}
242248
}
243249
}
@@ -326,7 +332,8 @@ async fn albatross_virtual_workspace() {
326332
"override-dependencies": null,
327333
"constraint-dependencies": null
328334
}
329-
}
335+
},
336+
"dependency-groups": null
330337
}
331338
}
332339
}
@@ -377,7 +384,8 @@ async fn albatross_just_project() {
377384
],
378385
"optional-dependencies": null
379386
},
380-
"tool": null
387+
"tool": null,
388+
"dependency-groups": null
381389
}
382390
}
383391
}
@@ -528,7 +536,8 @@ async fn exclude_package() -> Result<()> {
528536
"override-dependencies": null,
529537
"constraint-dependencies": null
530538
}
531-
}
539+
},
540+
"dependency-groups": null
532541
}
533542
}
534543
}
@@ -629,7 +638,8 @@ async fn exclude_package() -> Result<()> {
629638
"override-dependencies": null,
630639
"constraint-dependencies": null
631640
}
632-
}
641+
},
642+
"dependency-groups": null
633643
}
634644
}
635645
}
@@ -743,7 +753,8 @@ async fn exclude_package() -> Result<()> {
743753
"override-dependencies": null,
744754
"constraint-dependencies": null
745755
}
746-
}
756+
},
757+
"dependency-groups": null
747758
}
748759
}
749760
}
@@ -831,7 +842,8 @@ async fn exclude_package() -> Result<()> {
831842
"override-dependencies": null,
832843
"constraint-dependencies": null
833844
}
834-
}
845+
},
846+
"dependency-groups": null
835847
}
836848
}
837849
}
@@ -840,3 +852,21 @@ async fn exclude_package() -> Result<()> {
840852

841853
Ok(())
842854
}
855+
856+
#[test]
857+
fn read_dependency_groups() {
858+
let toml = r#"
859+
[dependency-groups]
860+
test = ["a"]
861+
"#;
862+
863+
let result =
864+
PyProjectToml::from_string(toml.to_string()).expect("Deserialization should succeed");
865+
let groups = result
866+
.dependency_groups
867+
.expect("`dependency-groups` should be present");
868+
let test = groups
869+
.get(&ExtraName::from_str("test").unwrap())
870+
.expect("Group `test` should be present");
871+
assert_eq!(test, &["a".to_string()]);
872+
}

crates/uv/src/commands/project/add.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ pub(crate) async fn add(
204204
bail!("Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead", "uv add --dev".green())
205205
}
206206
DependencyType::Dev => (),
207+
DependencyType::Group(_) => (),
207208
}
208209
}
209210

@@ -469,6 +470,7 @@ pub(crate) async fn add(
469470
DependencyType::Optional(ref group) => {
470471
toml.add_optional_dependency(group, &requirement, source.as_ref())?
471472
}
473+
DependencyType::Group(_) => todo!("adding dependencies to groups is not yet supported"),
472474
};
473475

474476
// If the edit was inserted before the end of the list, update the existing edits.
@@ -742,6 +744,9 @@ async fn lock_and_sync(
742744
DependencyType::Optional(ref group) => {
743745
toml.set_optional_dependency_minimum_version(group, *index, minimum)?;
744746
}
747+
DependencyType::Group(_) => {
748+
todo!("adding dependencies to groups is not yet supported")
749+
}
745750
}
746751

747752
modified = true;
@@ -813,6 +818,7 @@ async fn lock_and_sync(
813818
let dev = DevMode::Exclude;
814819
(extras, dev)
815820
}
821+
DependencyType::Group(_) => todo!("adding dependencies to groups is not yet supported"),
816822
};
817823

818824
project::sync::do_sync(

crates/uv/src/commands/project/remove.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ pub(crate) async fn remove(
122122
);
123123
}
124124
}
125+
DependencyType::Group(_) => {
126+
todo!("removing dependencies from groups is not yet supported")
127+
}
125128
}
126129
}
127130

@@ -249,6 +252,9 @@ fn warn_if_present(name: &PackageName, pyproject: &PyProjectTomlMut) {
249252
"`{name}` is an optional dependency; try calling `uv remove --optional {group}`",
250253
);
251254
}
255+
DependencyType::Group(_) => {
256+
// TODO(zanieb): Once we support `remove --group`, add a warning here.
257+
}
252258
}
253259
}
254260
}

crates/uv/src/settings.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,8 +843,8 @@ impl AddSettings {
843843
python,
844844
} = args;
845845

846-
let dependency_type = if let Some(group) = optional {
847-
DependencyType::Optional(group)
846+
let dependency_type = if let Some(extra) = optional {
847+
DependencyType::Optional(extra)
848848
} else if dev {
849849
DependencyType::Dev
850850
} else {

0 commit comments

Comments
 (0)