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.
This commit is contained in:
Zanie Blue 2024-10-16 16:34:45 -05:00
parent d2cd09bbd7
commit 3c9d783e09
6 changed files with 75 additions and 13 deletions

View File

@ -44,6 +44,8 @@ pub struct PyProjectToml {
pub project: Option<Project>, pub project: Option<Project>,
/// Tool-specific metadata. /// Tool-specific metadata.
pub tool: Option<Tool>, pub tool: Option<Tool>,
/// Non-project dependency groups, as defined in PEP 735.
pub dependency_groups: Option<BTreeMap<ExtraName, Vec<String>>>,
/// The raw unserialized document. /// The raw unserialized document.
#[serde(skip)] #[serde(skip)]
pub raw: String, pub raw: String,
@ -1141,6 +1143,8 @@ pub enum DependencyType {
Dev, Dev,
/// A dependency in `project.optional-dependencies.{0}`. /// A dependency in `project.optional-dependencies.{0}`.
Optional(ExtraName), Optional(ExtraName),
/// A dependency in `dependency-groups.{0}`.
Group(ExtraName),
} }
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452> /// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>

View File

@ -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`. // Check `tool.uv.dev-dependencies`.
if let Some(dev_dependencies) = self if let Some(dev_dependencies) = self
.doc .doc
@ -719,7 +735,7 @@ impl PyProjectTomlMut {
.and_then(Item::as_table) .and_then(Item::as_table)
.and_then(|tool| tool.get("uv")) .and_then(|tool| tool.get("uv"))
.and_then(Item::as_table) .and_then(Item::as_table)
.and_then(|tool| tool.get("dev-dependencies")) .and_then(|uv| uv.get("dev-dependencies"))
.and_then(Item::as_array) .and_then(Item::as_array)
{ {
if !find_dependencies(name, marker, dev_dependencies).is_empty() { if !find_dependencies(name, marker, dev_dependencies).is_empty() {

View File

@ -1,12 +1,15 @@
use std::env; use std::env;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use assert_fs::fixture::ChildPath; use assert_fs::fixture::ChildPath;
use assert_fs::prelude::*; use assert_fs::prelude::*;
use insta::assert_json_snapshot; use insta::assert_json_snapshot;
use uv_pep508::ExtraName;
use crate::pyproject::PyProjectToml;
use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; use crate::workspace::{DiscoveryOptions, ProjectWorkspace};
async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) {
@ -76,7 +79,8 @@ async fn albatross_in_example() {
], ],
"optional-dependencies": null "optional-dependencies": null
}, },
"tool": null "tool": null,
"dependency-groups": null
} }
} }
} }
@ -128,7 +132,8 @@ async fn albatross_project_in_excluded() {
], ],
"optional-dependencies": null "optional-dependencies": null
}, },
"tool": null "tool": null,
"dependency-groups": null
} }
} }
} }
@ -237,7 +242,8 @@ async fn albatross_root_workspace() {
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null "constraint-dependencies": null
} }
} },
"dependency-groups": null
} }
} }
} }
@ -326,7 +332,8 @@ async fn albatross_virtual_workspace() {
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null "constraint-dependencies": null
} }
} },
"dependency-groups": null
} }
} }
} }
@ -377,7 +384,8 @@ async fn albatross_just_project() {
], ],
"optional-dependencies": null "optional-dependencies": null
}, },
"tool": null "tool": null,
"dependency-groups": null
} }
} }
} }
@ -528,7 +536,8 @@ async fn exclude_package() -> Result<()> {
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null "constraint-dependencies": null
} }
} },
"dependency-groups": null
} }
} }
} }
@ -629,7 +638,8 @@ async fn exclude_package() -> Result<()> {
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null "constraint-dependencies": null
} }
} },
"dependency-groups": null
} }
} }
} }
@ -743,7 +753,8 @@ async fn exclude_package() -> Result<()> {
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null "constraint-dependencies": null
} }
} },
"dependency-groups": null
} }
} }
} }
@ -831,7 +842,8 @@ async fn exclude_package() -> Result<()> {
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null "constraint-dependencies": null
} }
} },
"dependency-groups": null
} }
} }
} }
@ -840,3 +852,21 @@ async fn exclude_package() -> Result<()> {
Ok(()) 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()]);
}

View File

@ -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()) 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::Dev => (),
DependencyType::Group(_) => (),
} }
} }
@ -469,6 +470,7 @@ pub(crate) async fn add(
DependencyType::Optional(ref group) => { DependencyType::Optional(ref group) => {
toml.add_optional_dependency(group, &requirement, source.as_ref())? 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. // 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) => { DependencyType::Optional(ref group) => {
toml.set_optional_dependency_minimum_version(group, *index, minimum)?; toml.set_optional_dependency_minimum_version(group, *index, minimum)?;
} }
DependencyType::Group(_) => {
todo!("adding dependencies to groups is not yet supported")
}
} }
modified = true; modified = true;
@ -817,6 +822,7 @@ async fn lock_and_sync(
let dev = DevMode::Exclude; let dev = DevMode::Exclude;
(extras, dev) (extras, dev)
} }
DependencyType::Group(_) => todo!("adding dependencies to groups is not yet supported"),
}; };
project::sync::do_sync( project::sync::do_sync(

View File

@ -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}`", "`{name}` is an optional dependency; try calling `uv remove --optional {group}`",
); );
} }
DependencyType::Group(_) => {
// TODO(zanieb): Once we support `remove --group`, add a warning here.
}
} }
} }
} }

View File

@ -849,8 +849,8 @@ impl AddSettings {
python, python,
} = args; } = args;
let dependency_type = if let Some(group) = optional { let dependency_type = if let Some(extra) = optional {
DependencyType::Optional(group) DependencyType::Optional(extra)
} else if dev { } else if dev {
DependencyType::Dev DependencyType::Dev
} else { } else {