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