use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::str::FromStr; use thiserror::Error; use tracing::error; use uv_normalize::{DEV_DEPENDENCIES, GroupName}; use uv_pep508::Pep508Error; use uv_pypi_types::{DependencyGroupSpecifier, VerbatimParsedUrl}; /// PEP 735 dependency groups, with any `include-group` entries resolved. #[derive(Debug, Default, Clone)] pub struct FlatDependencyGroups( BTreeMap>>, ); impl FlatDependencyGroups { /// Resolve the dependency groups (which may contain references to other groups) into concrete /// lists of requirements. pub fn from_dependency_groups( groups: &BTreeMap<&GroupName, &Vec>, ) -> Result { fn resolve_group<'data>( resolved: &mut BTreeMap>>, groups: &'data BTreeMap<&GroupName, &Vec>, name: &'data GroupName, parents: &mut Vec<&'data GroupName>, ) -> Result<(), DependencyGroupError> { let Some(specifiers) = groups.get(name) else { // Missing group let parent_name = parents .iter() .last() .copied() .expect("parent when group is missing"); return Err(DependencyGroupError::GroupNotFound( name.clone(), parent_name.clone(), )); }; // "Dependency Group Includes MUST NOT include cycles, and tools SHOULD report an error if they detect a cycle." if parents.contains(&name) { return Err(DependencyGroupError::DependencyGroupCycle(Cycle( parents.iter().copied().cloned().collect(), ))); } // If we already resolved this group, short-circuit. if resolved.contains_key(name) { return Ok(()); } parents.push(name); let mut requirements = Vec::with_capacity(specifiers.len()); for specifier in *specifiers { match specifier { DependencyGroupSpecifier::Requirement(requirement) => { match uv_pep508::Requirement::::from_str(requirement) { Ok(requirement) => requirements.push(requirement), Err(err) => { return Err(DependencyGroupError::GroupParseError( name.clone(), requirement.clone(), Box::new(err), )); } } } DependencyGroupSpecifier::IncludeGroup { include_group } => { resolve_group(resolved, groups, include_group, parents)?; requirements .extend(resolved.get(include_group).into_iter().flatten().cloned()); } DependencyGroupSpecifier::Object(map) => { return Err(DependencyGroupError::DependencyObjectSpecifierNotSupported( name.clone(), map.clone(), )); } } } parents.pop(); resolved.insert(name.clone(), requirements); Ok(()) } let mut resolved = BTreeMap::new(); for name in groups.keys() { let mut parents = Vec::new(); resolve_group(&mut resolved, groups, name, &mut parents)?; } Ok(Self(resolved)) } /// Return the requirements for a given group, if any. pub fn get( &self, group: &GroupName, ) -> Option<&Vec>> { self.0.get(group) } /// Return the entry for a given group, if any. pub fn entry( &mut self, group: GroupName, ) -> Entry>> { self.0.entry(group) } /// Consume the [`FlatDependencyGroups`] and return the inner map. pub fn into_inner(self) -> BTreeMap>> { self.0 } } impl FromIterator<(GroupName, Vec>)> for FlatDependencyGroups { fn from_iter< T: IntoIterator>)>, >( iter: T, ) -> Self { Self(iter.into_iter().collect()) } } impl IntoIterator for FlatDependencyGroups { type Item = (GroupName, Vec>); type IntoIter = std::collections::btree_map::IntoIter< GroupName, Vec>, >; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } #[derive(Debug, Error)] pub enum DependencyGroupError { #[error("Failed to parse entry in group `{0}`: `{1}`")] GroupParseError( GroupName, String, #[source] Box>, ), #[error("Failed to find group `{0}` included by `{1}`")] GroupNotFound(GroupName, GroupName), #[error( "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." )] DevGroupInclude(GroupName), #[error("Detected a cycle in `dependency-groups`: {0}")] DependencyGroupCycle(Cycle), #[error("Group `{0}` contains an unknown dependency object specifier: {1:?}")] DependencyObjectSpecifierNotSupported(GroupName, BTreeMap), } impl DependencyGroupError { /// Enrich a [`DependencyGroupError`] with the `tool.uv.dev-dependencies` metadata, if applicable. #[must_use] pub fn with_dev_dependencies( self, dev_dependencies: Option<&Vec>>, ) -> Self { match self { DependencyGroupError::GroupNotFound(group, parent) if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES => { DependencyGroupError::DevGroupInclude(parent) } _ => self, } } } /// A cycle in the `dependency-groups` table. #[derive(Debug)] pub struct Cycle(Vec); /// Display a cycle, e.g., `a -> b -> c -> a`. impl std::fmt::Display for Cycle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let [first, rest @ ..] = self.0.as_slice() else { return Ok(()); }; write!(f, "`{first}`")?; for group in rest { write!(f, " -> `{group}`")?; } write!(f, " -> `{first}`")?; Ok(()) } }