pyproject_toml/
pep735_resolve.rs1use indexmap::IndexMap;
2use pep508_rs::Requirement;
3use thiserror::Error;
4
5use crate::{DependencyGroupSpecifier, DependencyGroups};
6
7#[derive(Debug, Error)]
8pub enum Pep735Error {
9 #[error("Failed to find group `{0}` included by `{1}`")]
10 GroupNotFound(String, String),
11 #[error("Detected a cycle in `dependency-groups`: {0}")]
12 DependencyGroupCycle(Cycle),
13}
14
15#[derive(Debug)]
17pub struct Cycle(Vec<String>);
18
19impl std::fmt::Display for Cycle {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 let [first, rest @ ..] = self.0.as_slice() else {
23 return Ok(());
24 };
25 write!(f, "`{first}`")?;
26 for group in rest {
27 write!(f, " -> `{group}`")?;
28 }
29 write!(f, " -> `{first}`")?;
30 Ok(())
31 }
32}
33
34impl DependencyGroups {
35 pub fn resolve(&self) -> Result<IndexMap<String, Vec<Requirement>>, Pep735Error> {
38 fn resolve_single<'a>(
40 groups: &'a DependencyGroups,
41 group: &'a str,
42 resolved: &mut IndexMap<String, Vec<Requirement>>,
43 parents: &mut Vec<&'a str>,
44 ) -> Result<(), Pep735Error> {
45 let Some(specifiers) = groups.get(group) else {
46 let parent = parents.iter().last().expect("should have a parent");
48 return Err(Pep735Error::GroupNotFound(
49 group.to_string(),
50 parent.to_string(),
51 ));
52 };
53 if parents.contains(&group) {
55 return Err(Pep735Error::DependencyGroupCycle(Cycle(
56 parents.iter().map(|s| s.to_string()).collect(),
57 )));
58 }
59 if resolved.get(group).is_some() {
61 return Ok(());
62 }
63 parents.push(group);
65 let mut requirements = Vec::with_capacity(specifiers.len());
66 for spec in specifiers.iter() {
67 match spec {
68 DependencyGroupSpecifier::String(requirement) => {
70 requirements.push(requirement.clone())
71 }
72 DependencyGroupSpecifier::Table { include_group } => {
74 resolve_single(groups, include_group, resolved, parents)?;
75 requirements
76 .extend(resolved.get(include_group).into_iter().flatten().cloned());
77 }
78 }
79 }
80 resolved.insert(group.to_string(), requirements.clone());
82 parents.pop();
83 Ok(())
84 }
85
86 let mut resolved = IndexMap::new();
87 for group in self.keys() {
88 resolve_single(self, group, &mut resolved, &mut Vec::new())?;
89 }
90 Ok(resolved)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use pep508_rs::Requirement;
97 use std::str::FromStr;
98
99 use crate::PyProjectToml;
100
101 #[test]
102 fn test_parse_pyproject_toml_dependency_groups_resolve() {
103 let source = r#"[dependency-groups]
104alpha = ["beta", "gamma", "delta"]
105epsilon = ["eta<2.0", "theta==2024.09.01"]
106iota = [{include-group = "alpha"}]
107"#;
108 let project_toml = PyProjectToml::new(source).unwrap();
109 let dependency_groups = project_toml.dependency_groups.as_ref().unwrap();
110
111 assert_eq!(
112 dependency_groups.resolve().unwrap()["iota"],
113 vec![
114 Requirement::from_str("beta").unwrap(),
115 Requirement::from_str("gamma").unwrap(),
116 Requirement::from_str("delta").unwrap()
117 ]
118 );
119 }
120
121 #[test]
122 fn test_parse_pyproject_toml_dependency_groups_cycle() {
123 let source = r#"[dependency-groups]
124alpha = [{include-group = "iota"}]
125iota = [{include-group = "alpha"}]
126"#;
127 let project_toml = PyProjectToml::new(source).unwrap();
128 let dependency_groups = project_toml.dependency_groups.as_ref().unwrap();
129 assert_eq!(
130 dependency_groups.resolve().unwrap_err().to_string(),
131 String::from("Detected a cycle in `dependency-groups`: `alpha` -> `iota` -> `alpha`")
132 )
133 }
134
135 #[test]
136 fn test_parse_pyproject_toml_dependency_groups_missing_include() {
137 let source = r#"[dependency-groups]
138iota = [{include-group = "alpha"}]
139"#;
140 let project_toml = PyProjectToml::new(source).unwrap();
141 let dependency_groups = project_toml.dependency_groups.as_ref().unwrap();
142 assert_eq!(
143 dependency_groups.resolve().unwrap_err().to_string(),
144 String::from("Failed to find group `alpha` included by `iota`")
145 )
146 }
147}