1use std::collections::btree_map::Entry;
2use std::str::FromStr;
3use std::{collections::BTreeMap, path::Path};
4
5use thiserror::Error;
6
7use uv_distribution_types::RequiresPython;
8use uv_fs::Simplified;
9use uv_normalize::{DEV_DEPENDENCIES, GroupName};
10use uv_pep440::VersionSpecifiers;
11use uv_pep508::Pep508Error;
12use uv_pypi_types::{DependencyGroupSpecifier, VerbatimParsedUrl};
13
14use crate::pyproject::{DependencyGroupSettings, PyProjectToml, ToolUvDependencyGroups};
15
16#[derive(Debug, Default, Clone)]
18pub struct FlatDependencyGroups(BTreeMap<GroupName, FlatDependencyGroup>);
19
20#[derive(Debug, Default, Clone)]
21pub struct FlatDependencyGroup {
22 pub requirements: Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
23 pub requires_python: Option<VersionSpecifiers>,
24}
25
26impl FlatDependencyGroups {
27 pub fn from_pyproject_toml(
31 path: &Path,
32 pyproject_toml: &PyProjectToml,
33 ) -> Result<Self, DependencyGroupError> {
34 let dev_dependencies = pyproject_toml
36 .tool
37 .as_ref()
38 .and_then(|tool| tool.uv.as_ref())
39 .and_then(|uv| uv.dev_dependencies.as_ref());
40
41 let dependency_groups = pyproject_toml
43 .dependency_groups
44 .iter()
45 .flatten()
46 .collect::<BTreeMap<_, _>>();
47
48 let empty_settings = ToolUvDependencyGroups::default();
50 let group_settings = pyproject_toml
51 .tool
52 .as_ref()
53 .and_then(|tool| tool.uv.as_ref())
54 .and_then(|uv| uv.dependency_groups.as_ref())
55 .unwrap_or(&empty_settings);
56
57 let mut dependency_groups =
59 Self::from_dependency_groups(&dependency_groups, group_settings.inner()).map_err(
60 |err| DependencyGroupError {
61 package: pyproject_toml
62 .project
63 .as_ref()
64 .map(|project| project.name.to_string())
65 .unwrap_or_default(),
66 path: path.user_display().to_string(),
67 error: err.with_dev_dependencies(dev_dependencies),
68 },
69 )?;
70
71 if let Some(dev_dependencies) = dev_dependencies {
79 dependency_groups
80 .entry(DEV_DEPENDENCIES.clone())
81 .or_insert_with(FlatDependencyGroup::default)
82 .requirements
83 .extend(dev_dependencies.clone());
84 }
85
86 Ok(dependency_groups)
87 }
88
89 fn from_dependency_groups(
92 groups: &BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
93 settings: &BTreeMap<GroupName, DependencyGroupSettings>,
94 ) -> Result<Self, DependencyGroupErrorInner> {
95 fn resolve_group<'data>(
96 resolved: &mut BTreeMap<GroupName, FlatDependencyGroup>,
97 groups: &'data BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
98 settings: &BTreeMap<GroupName, DependencyGroupSettings>,
99 name: &'data GroupName,
100 parents: &mut Vec<&'data GroupName>,
101 ) -> Result<(), DependencyGroupErrorInner> {
102 let Some(specifiers) = groups.get(name) else {
103 let parent_name = parents
105 .iter()
106 .last()
107 .copied()
108 .expect("parent when group is missing");
109 return Err(DependencyGroupErrorInner::GroupNotFound(
110 name.clone(),
111 parent_name.clone(),
112 ));
113 };
114
115 if parents.contains(&name) {
117 return Err(DependencyGroupErrorInner::DependencyGroupCycle(Cycle(
118 parents.iter().copied().cloned().collect(),
119 )));
120 }
121
122 if resolved.contains_key(name) {
124 return Ok(());
125 }
126
127 parents.push(name);
128 let mut requirements = Vec::with_capacity(specifiers.len());
129 let mut requires_python_intersection = VersionSpecifiers::empty();
130 for specifier in *specifiers {
131 match specifier {
132 DependencyGroupSpecifier::Requirement(requirement) => {
133 match uv_pep508::Requirement::<VerbatimParsedUrl>::from_str(requirement) {
134 Ok(requirement) => requirements.push(requirement),
135 Err(err) => {
136 return Err(DependencyGroupErrorInner::GroupParseError(
137 name.clone(),
138 requirement.clone(),
139 Box::new(err),
140 ));
141 }
142 }
143 }
144 DependencyGroupSpecifier::IncludeGroup { include_group } => {
145 resolve_group(resolved, groups, settings, include_group, parents)?;
146 if let Some(included) = resolved.get(include_group) {
147 requirements.extend(included.requirements.iter().cloned());
148
149 requires_python_intersection = requires_python_intersection
151 .into_iter()
152 .chain(included.requires_python.clone().into_iter().flatten())
153 .collect();
154 }
155 }
156 DependencyGroupSpecifier::Object(map) => {
157 return Err(
158 DependencyGroupErrorInner::DependencyObjectSpecifierNotSupported(
159 name.clone(),
160 map.clone(),
161 ),
162 );
163 }
164 }
165 }
166
167 let empty_settings = DependencyGroupSettings::default();
168 let DependencyGroupSettings { requires_python } =
169 settings.get(name).unwrap_or(&empty_settings);
170 if let Some(requires_python) = requires_python {
171 requires_python_intersection = requires_python_intersection
174 .into_iter()
175 .chain(requires_python.clone())
176 .collect();
177
178 for requirement in &mut requirements {
182 let extra_markers =
183 RequiresPython::from_specifiers(requires_python).to_marker_tree();
184 requirement.marker.and(extra_markers);
185 }
186 }
187
188 parents.pop();
189
190 resolved.insert(
191 name.clone(),
192 FlatDependencyGroup {
193 requirements,
194 requires_python: if requires_python_intersection.is_empty() {
195 None
196 } else {
197 Some(requires_python_intersection)
198 },
199 },
200 );
201 Ok(())
202 }
203
204 for (group_name, ..) in settings {
206 if !groups.contains_key(group_name) {
207 return Err(DependencyGroupErrorInner::SettingsGroupNotFound(
208 group_name.clone(),
209 ));
210 }
211 }
212
213 let mut resolved = BTreeMap::new();
214 for name in groups.keys() {
215 let mut parents = Vec::new();
216 resolve_group(&mut resolved, groups, settings, name, &mut parents)?;
217 }
218 Ok(Self(resolved))
219 }
220
221 pub fn get(&self, group: &GroupName) -> Option<&FlatDependencyGroup> {
223 self.0.get(group)
224 }
225
226 pub fn entry(&mut self, group: GroupName) -> Entry<'_, GroupName, FlatDependencyGroup> {
228 self.0.entry(group)
229 }
230
231 pub fn into_inner(self) -> BTreeMap<GroupName, FlatDependencyGroup> {
233 self.0
234 }
235}
236
237impl FromIterator<(GroupName, FlatDependencyGroup)> for FlatDependencyGroups {
238 fn from_iter<T: IntoIterator<Item = (GroupName, FlatDependencyGroup)>>(iter: T) -> Self {
239 Self(iter.into_iter().collect())
240 }
241}
242
243impl IntoIterator for FlatDependencyGroups {
244 type Item = (GroupName, FlatDependencyGroup);
245 type IntoIter = std::collections::btree_map::IntoIter<GroupName, FlatDependencyGroup>;
246
247 fn into_iter(self) -> Self::IntoIter {
248 self.0.into_iter()
249 }
250}
251
252#[derive(Debug, Error)]
253#[error("{} has malformed dependency groups", if path.is_empty() && package.is_empty() {
254 "Project".to_string()
255} else if path.is_empty() || path == "." {
256 format!("Project `{package}`")
257} else if package.is_empty() {
258 format!("`{path}`")
259} else {
260 format!("Project `{package} @ {path}`")
261})]
262pub struct DependencyGroupError {
263 package: String,
264 path: String,
265 #[source]
266 error: DependencyGroupErrorInner,
267}
268
269#[derive(Debug, Error)]
270pub enum DependencyGroupErrorInner {
271 #[error("Failed to parse entry in group `{0}`: `{1}`")]
272 GroupParseError(
273 GroupName,
274 String,
275 #[source] Box<Pep508Error<VerbatimParsedUrl>>,
276 ),
277 #[error("Failed to find group `{0}` included by `{1}`")]
278 GroupNotFound(GroupName, GroupName),
279 #[error(
280 "Group `{0}` includes the `dev` group (`include = \"dev\"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead."
281 )]
282 DevGroupInclude(GroupName),
283 #[error("Detected a cycle in `dependency-groups`: {0}")]
284 DependencyGroupCycle(Cycle),
285 #[error("Group `{0}` contains an unknown dependency object specifier: {1:?}")]
286 DependencyObjectSpecifierNotSupported(GroupName, BTreeMap<String, String>),
287 #[error("Failed to find group `{0}` specified in `[tool.uv.dependency-groups]`")]
288 SettingsGroupNotFound(GroupName),
289 #[error(
290 "`[tool.uv.dependency-groups]` specifies the `dev` group, but only `tool.uv.dev-dependencies` was found. To reference the `dev` group, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead."
291 )]
292 SettingsDevGroupInclude,
293}
294
295impl DependencyGroupErrorInner {
296 #[must_use]
298 pub fn with_dev_dependencies(
299 self,
300 dev_dependencies: Option<&Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
301 ) -> Self {
302 match self {
303 Self::GroupNotFound(group, parent)
304 if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES =>
305 {
306 Self::DevGroupInclude(parent)
307 }
308 Self::SettingsGroupNotFound(group)
309 if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES =>
310 {
311 Self::SettingsDevGroupInclude
312 }
313 _ => self,
314 }
315 }
316}
317
318#[derive(Debug)]
320pub struct Cycle(Vec<GroupName>);
321
322impl std::fmt::Display for Cycle {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 let [first, rest @ ..] = self.0.as_slice() else {
326 return Ok(());
327 };
328 write!(f, "`{first}`")?;
329 for group in rest {
330 write!(f, " -> `{group}`")?;
331 }
332 write!(f, " -> `{first}`")?;
333 Ok(())
334 }
335}