mirror of https://github.com/astral-sh/uv
Allow `[dependency-groups]` in non-`[project]` projects (#8574)
## Summary We already support `tool.uv.dev-dependencies` in the legacy non-`[project]` projects. This adds equivalent support for `[dependency-groups]`, e.g.: ```toml [tool.uv.workspace] [dependency-groups] lint = ["ruff"] ```
This commit is contained in:
parent
4df9ab2b58
commit
dd0f696695
|
|
@ -7,8 +7,8 @@ use uv_configuration::{LowerBound, SourceStrategy};
|
||||||
use uv_distribution_types::IndexLocations;
|
use uv_distribution_types::IndexLocations;
|
||||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||||
use uv_pep440::{Version, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifiers};
|
||||||
use uv_pep508::Pep508Error;
|
use uv_pypi_types::{HashDigest, ResolutionMetadata};
|
||||||
use uv_pypi_types::{HashDigest, ResolutionMetadata, VerbatimParsedUrl};
|
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||||
use uv_workspace::WorkspaceError;
|
use uv_workspace::WorkspaceError;
|
||||||
|
|
||||||
pub use crate::metadata::lowering::LoweredRequirement;
|
pub use crate::metadata::lowering::LoweredRequirement;
|
||||||
|
|
@ -22,39 +22,12 @@ mod requires_dist;
|
||||||
pub enum MetadataError {
|
pub enum MetadataError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Workspace(#[from] WorkspaceError),
|
Workspace(#[from] WorkspaceError),
|
||||||
|
#[error(transparent)]
|
||||||
|
DependencyGroup(#[from] DependencyGroupError),
|
||||||
#[error("Failed to parse entry: `{0}`")]
|
#[error("Failed to parse entry: `{0}`")]
|
||||||
LoweringError(PackageName, #[source] Box<LoweringError>),
|
LoweringError(PackageName, #[source] Box<LoweringError>),
|
||||||
#[error("Failed to parse entry in group `{0}`: `{1}`")]
|
#[error("Failed to parse entry in group `{0}`: `{1}`")]
|
||||||
GroupLoweringError(GroupName, PackageName, #[source] Box<LoweringError>),
|
GroupLoweringError(GroupName, PackageName, #[source] Box<LoweringError>),
|
||||||
#[error("Failed to parse entry in group `{0}`: `{1}`")]
|
|
||||||
GroupParseError(
|
|
||||||
GroupName,
|
|
||||||
String,
|
|
||||||
#[source] Box<Pep508Error<VerbatimParsedUrl>>,
|
|
||||||
),
|
|
||||||
#[error("Failed to find group `{0}` included by `{1}`")]
|
|
||||||
GroupNotFound(GroupName, GroupName),
|
|
||||||
#[error("Detected a cycle in `dependency-groups`: {0}")]
|
|
||||||
DependencyGroupCycle(Cycle),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A cycle in the `dependency-groups` table.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Cycle(Vec<GroupName>);
|
|
||||||
|
|
||||||
/// 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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
|
use crate::metadata::{LoweredRequirement, MetadataError};
|
||||||
|
use crate::Metadata;
|
||||||
use uv_configuration::{LowerBound, SourceStrategy};
|
use uv_configuration::{LowerBound, SourceStrategy};
|
||||||
use uv_distribution_types::IndexLocations;
|
use uv_distribution_types::IndexLocations;
|
||||||
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
||||||
use uv_pypi_types::VerbatimParsedUrl;
|
use uv_workspace::dependency_groups::FlatDependencyGroups;
|
||||||
use uv_workspace::pyproject::{DependencyGroupSpecifier, ToolUvSources};
|
use uv_workspace::pyproject::ToolUvSources;
|
||||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
||||||
|
|
||||||
use crate::metadata::{Cycle, LoweredRequirement, MetadataError};
|
|
||||||
use crate::Metadata;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RequiresDist {
|
pub struct RequiresDist {
|
||||||
pub name: PackageName,
|
pub name: PackageName,
|
||||||
|
|
@ -121,54 +117,55 @@ impl RequiresDist {
|
||||||
.collect::<BTreeMap<_, _>>();
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
// Resolve any `include-group` entries in `dependency-groups`.
|
// Resolve any `include-group` entries in `dependency-groups`.
|
||||||
let dependency_groups = resolve_dependency_groups(&dependency_groups)?
|
let dependency_groups =
|
||||||
.into_iter()
|
FlatDependencyGroups::from_dependency_groups(&dependency_groups)?
|
||||||
.chain(
|
.into_iter()
|
||||||
// Only add the `dev` group if `dev-dependencies` is defined.
|
.chain(
|
||||||
dev_dependencies
|
// Only add the `dev` group if `dev-dependencies` is defined.
|
||||||
.into_iter()
|
dev_dependencies
|
||||||
.map(|requirements| (DEV_DEPENDENCIES.clone(), requirements.clone())),
|
|
||||||
)
|
|
||||||
.map(|(name, requirements)| {
|
|
||||||
let requirements = match source_strategy {
|
|
||||||
SourceStrategy::Enabled => requirements
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|requirement| {
|
.map(|requirements| (DEV_DEPENDENCIES.clone(), requirements.clone())),
|
||||||
let group_name = name.clone();
|
)
|
||||||
let requirement_name = requirement.name.clone();
|
.map(|(name, requirements)| {
|
||||||
LoweredRequirement::from_requirement(
|
let requirements = match source_strategy {
|
||||||
requirement,
|
SourceStrategy::Enabled => requirements
|
||||||
&metadata.name,
|
.into_iter()
|
||||||
project_workspace.project_root(),
|
.flat_map(|requirement| {
|
||||||
project_sources,
|
let group_name = name.clone();
|
||||||
project_indexes,
|
let requirement_name = requirement.name.clone();
|
||||||
locations,
|
LoweredRequirement::from_requirement(
|
||||||
project_workspace.workspace(),
|
requirement,
|
||||||
lower_bound,
|
&metadata.name,
|
||||||
)
|
project_workspace.project_root(),
|
||||||
.map(move |requirement| {
|
project_sources,
|
||||||
match requirement {
|
project_indexes,
|
||||||
Ok(requirement) => Ok(requirement.into_inner()),
|
locations,
|
||||||
Err(err) => Err(MetadataError::GroupLoweringError(
|
project_workspace.workspace(),
|
||||||
group_name.clone(),
|
lower_bound,
|
||||||
requirement_name.clone(),
|
)
|
||||||
Box::new(err),
|
.map(move |requirement| {
|
||||||
)),
|
match requirement {
|
||||||
}
|
Ok(requirement) => Ok(requirement.into_inner()),
|
||||||
|
Err(err) => Err(MetadataError::GroupLoweringError(
|
||||||
|
group_name.clone(),
|
||||||
|
requirement_name.clone(),
|
||||||
|
Box::new(err),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.collect::<Result<Vec<_>, _>>(),
|
||||||
.collect::<Result<Vec<_>, _>>(),
|
SourceStrategy::Disabled => Ok(requirements
|
||||||
SourceStrategy::Disabled => Ok(requirements
|
.into_iter()
|
||||||
.into_iter()
|
.map(uv_pypi_types::Requirement::from)
|
||||||
.map(uv_pypi_types::Requirement::from)
|
.collect()),
|
||||||
.collect()),
|
}?;
|
||||||
}?;
|
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
|
||||||
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
|
name,
|
||||||
name,
|
requirements,
|
||||||
requirements,
|
))
|
||||||
))
|
})
|
||||||
})
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
// Merge any overlapping groups.
|
// Merge any overlapping groups.
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
|
|
@ -235,81 +232,6 @@ impl From<Metadata> for RequiresDist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the dependency groups (which may contain references to other groups) into concrete
|
|
||||||
/// lists of requirements.
|
|
||||||
fn resolve_dependency_groups(
|
|
||||||
groups: &BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
|
|
||||||
) -> Result<BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>, MetadataError> {
|
|
||||||
fn resolve_group<'data>(
|
|
||||||
resolved: &mut BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
|
||||||
groups: &'data BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
|
|
||||||
name: &'data GroupName,
|
|
||||||
parents: &mut Vec<&'data GroupName>,
|
|
||||||
) -> Result<(), MetadataError> {
|
|
||||||
let Some(specifiers) = groups.get(name) else {
|
|
||||||
// Missing group
|
|
||||||
let parent_name = parents
|
|
||||||
.iter()
|
|
||||||
.last()
|
|
||||||
.copied()
|
|
||||||
.expect("parent when group is missing");
|
|
||||||
return Err(MetadataError::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(MetadataError::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::<VerbatimParsedUrl>::from_str(requirement) {
|
|
||||||
Ok(requirement) => requirements.push(requirement),
|
|
||||||
Err(err) => {
|
|
||||||
return Err(MetadataError::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) => {
|
|
||||||
warn!("Ignoring Dependency Object Specifier referenced by `{name}`: {map:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(resolved)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,14 @@ use std::sync::{Arc, LazyLock};
|
||||||
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
pub use crate::lock::requirements_txt::RequirementsTxtExport;
|
||||||
|
pub use crate::lock::tree::TreeDisplay;
|
||||||
|
use crate::requires_python::SimplifiedMarkerTree;
|
||||||
|
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
|
||||||
|
use crate::{
|
||||||
|
ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, RequiresPython, ResolutionGraph,
|
||||||
|
ResolutionMode,
|
||||||
|
};
|
||||||
use uv_cache_key::RepositoryUrl;
|
use uv_cache_key::RepositoryUrl;
|
||||||
use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions};
|
use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions};
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::DistributionDatabase;
|
||||||
|
|
@ -35,17 +43,9 @@ use uv_pypi_types::{
|
||||||
ResolverMarkerEnvironment,
|
ResolverMarkerEnvironment,
|
||||||
};
|
};
|
||||||
use uv_types::{BuildContext, HashStrategy};
|
use uv_types::{BuildContext, HashStrategy};
|
||||||
|
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||||
use uv_workspace::{InstallTarget, Workspace};
|
use uv_workspace::{InstallTarget, Workspace};
|
||||||
|
|
||||||
pub use crate::lock::requirements_txt::RequirementsTxtExport;
|
|
||||||
pub use crate::lock::tree::TreeDisplay;
|
|
||||||
use crate::requires_python::SimplifiedMarkerTree;
|
|
||||||
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
|
|
||||||
use crate::{
|
|
||||||
ExcludeNewer, InMemoryIndex, MetadataResponse, PrereleaseMode, RequiresPython, ResolutionGraph,
|
|
||||||
ResolutionMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod requirements_txt;
|
mod requirements_txt;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
|
|
@ -638,8 +638,11 @@ impl Lock {
|
||||||
|
|
||||||
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
|
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
|
||||||
// dependencies in (legacy) non-project workspace roots).
|
// dependencies in (legacy) non-project workspace roots).
|
||||||
|
let groups = project
|
||||||
|
.groups()
|
||||||
|
.map_err(|err| LockErrorKind::DependencyGroup { err })?;
|
||||||
for group in dev.iter() {
|
for group in dev.iter() {
|
||||||
for dependency in project.group(group) {
|
for dependency in groups.get(group).into_iter().flatten() {
|
||||||
if dependency.marker.evaluate(marker_env, &[]) {
|
if dependency.marker.evaluate(marker_env, &[]) {
|
||||||
let root_name = &dependency.name;
|
let root_name = &dependency.name;
|
||||||
let root = self
|
let root = self
|
||||||
|
|
@ -4135,6 +4138,11 @@ enum LockErrorKind {
|
||||||
#[source]
|
#[source]
|
||||||
err: uv_distribution::Error,
|
err: uv_distribution::Error,
|
||||||
},
|
},
|
||||||
|
#[error("Failed to resolve `dependency-groups`")]
|
||||||
|
DependencyGroup {
|
||||||
|
#[source]
|
||||||
|
err: DependencyGroupError,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurs when a source string could not be parsed.
|
/// An error that occurs when a source string could not be parsed.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use uv_normalize::GroupName;
|
||||||
|
use uv_pep508::Pep508Error;
|
||||||
|
use uv_pypi_types::VerbatimParsedUrl;
|
||||||
|
|
||||||
|
use crate::pyproject::DependencyGroupSpecifier;
|
||||||
|
|
||||||
|
/// PEP 735 dependency groups, with any `include-group` entries resolved.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FlatDependencyGroups(
|
||||||
|
BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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<DependencyGroupSpecifier>>,
|
||||||
|
) -> Result<Self, DependencyGroupError> {
|
||||||
|
fn resolve_group<'data>(
|
||||||
|
resolved: &mut BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||||
|
groups: &'data BTreeMap<&GroupName, &Vec<DependencyGroupSpecifier>>,
|
||||||
|
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::<VerbatimParsedUrl>::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) => {
|
||||||
|
warn!(
|
||||||
|
"Ignoring Dependency Object Specifier referenced by `{name}`: {map:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||||
|
self.0.get(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for FlatDependencyGroups {
|
||||||
|
type Item = (GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>);
|
||||||
|
type IntoIter = std::collections::btree_map::IntoIter<
|
||||||
|
GroupName,
|
||||||
|
Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<Pep508Error<VerbatimParsedUrl>>,
|
||||||
|
),
|
||||||
|
#[error("Failed to find group `{0}` included by `{1}`")]
|
||||||
|
GroupNotFound(GroupName, GroupName),
|
||||||
|
#[error("Detected a cycle in `dependency-groups`: {0}")]
|
||||||
|
DependencyGroupCycle(Cycle),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cycle in the `dependency-groups` table.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Cycle(Vec<GroupName>);
|
||||||
|
|
||||||
|
/// 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ pub use workspace::{
|
||||||
VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
|
VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod dependency_groups;
|
||||||
pub mod pyproject;
|
pub mod pyproject;
|
||||||
pub mod pyproject_mut;
|
pub mod pyproject_mut;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`].
|
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`].
|
||||||
|
|
||||||
|
use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroups};
|
||||||
|
use crate::pyproject::{
|
||||||
|
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
|
||||||
|
};
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use glob::{glob, GlobError, PatternError};
|
use glob::{glob, GlobError, PatternError};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
@ -14,10 +18,6 @@ use uv_pypi_types::{Requirement, RequirementSource, SupportedEnvironments, Verba
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
|
|
||||||
use crate::pyproject::{
|
|
||||||
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum WorkspaceError {
|
pub enum WorkspaceError {
|
||||||
// Workspace structure errors.
|
// Workspace structure errors.
|
||||||
|
|
@ -305,7 +305,7 @@ impl Workspace {
|
||||||
/// `pyproject.toml`.
|
/// `pyproject.toml`.
|
||||||
///
|
///
|
||||||
/// Otherwise, returns an empty list.
|
/// Otherwise, returns an empty list.
|
||||||
pub fn non_project_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
|
pub fn non_project_requirements(&self) -> Result<Vec<Requirement>, DependencyGroupError> {
|
||||||
if self
|
if self
|
||||||
.packages
|
.packages
|
||||||
.values()
|
.values()
|
||||||
|
|
@ -313,25 +313,46 @@ impl Workspace {
|
||||||
{
|
{
|
||||||
// If the workspace has an explicit root, the root is a member, so we don't need to
|
// If the workspace has an explicit root, the root is a member, so we don't need to
|
||||||
// include any root-only requirements.
|
// include any root-only requirements.
|
||||||
Either::Left(std::iter::empty())
|
Ok(Vec::new())
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, return the dev dependencies in the non-project workspace root.
|
// Otherwise, return the dependency groups in the non-project workspace root.
|
||||||
Either::Right(
|
// First, collect `tool.uv.dev_dependencies`
|
||||||
self.pyproject_toml
|
let dev_dependencies = self
|
||||||
.tool
|
.pyproject_toml
|
||||||
.as_ref()
|
.tool
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
.as_ref()
|
||||||
.and_then(|uv| uv.dev_dependencies.as_ref())
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
.into_iter()
|
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||||
.flatten()
|
|
||||||
.map(|requirement| {
|
// Then, collect `dependency-groups`
|
||||||
Requirement::from(
|
let dependency_groups = self
|
||||||
requirement
|
.pyproject_toml
|
||||||
.clone()
|
.dependency_groups
|
||||||
.with_origin(RequirementOrigin::Workspace),
|
.iter()
|
||||||
)
|
.flatten()
|
||||||
}),
|
.collect::<BTreeMap<_, _>>();
|
||||||
)
|
|
||||||
|
// Resolve any `include-group` entries in `dependency-groups`.
|
||||||
|
let dependency_groups =
|
||||||
|
FlatDependencyGroups::from_dependency_groups(&dependency_groups)?;
|
||||||
|
|
||||||
|
// Concatenate the two sets of requirements.
|
||||||
|
let dev_dependencies = dependency_groups
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(_, requirements)| requirements)
|
||||||
|
.map(|requirement| {
|
||||||
|
Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
|
||||||
|
})
|
||||||
|
.chain(dev_dependencies.into_iter().flatten().map(|requirement| {
|
||||||
|
Requirement::from(
|
||||||
|
requirement
|
||||||
|
.clone()
|
||||||
|
.with_origin(RequirementOrigin::Workspace),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(dev_dependencies)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1434,7 +1455,7 @@ impl VirtualProject {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A target that can be installed.
|
/// A target that can be installed.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum InstallTarget<'env> {
|
pub enum InstallTarget<'env> {
|
||||||
/// A project (which could be a workspace root or member).
|
/// A project (which could be a workspace root or member).
|
||||||
Project(&'env ProjectWorkspace),
|
Project(&'env ProjectWorkspace),
|
||||||
|
|
@ -1468,38 +1489,62 @@ impl<'env> InstallTarget<'env> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`InstallTarget`] dependencies for the given group name.
|
/// Return the [`InstallTarget`] dependency groups.
|
||||||
///
|
///
|
||||||
/// Returns dependencies that apply to the workspace root, but not any of its members. As such,
|
/// Returns dependencies that apply to the workspace root, but not any of its members. As such,
|
||||||
/// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies
|
/// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies
|
||||||
/// on the virtual root.
|
/// on the virtual root.
|
||||||
pub fn group(
|
pub fn groups(
|
||||||
&self,
|
&self,
|
||||||
name: &GroupName,
|
) -> Result<
|
||||||
) -> impl Iterator<Item = &uv_pep508::Requirement<VerbatimParsedUrl>> {
|
BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||||
|
DependencyGroupError,
|
||||||
|
> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project(_) | Self::FrozenMember(..) => {
|
Self::Project(_) | Self::FrozenMember(..) => Ok(BTreeMap::new()),
|
||||||
// For projects, dev dependencies are attached to the members.
|
|
||||||
Either::Left(std::iter::empty())
|
|
||||||
}
|
|
||||||
Self::NonProject(workspace) => {
|
Self::NonProject(workspace) => {
|
||||||
// For non-projects, we might have dev dependencies that are attached to the
|
// For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies`
|
||||||
// workspace root (which isn't a member).
|
// that are attached to the workspace root (which isn't a member).
|
||||||
if name == &*DEV_DEPENDENCIES {
|
|
||||||
Either::Right(
|
// First, collect `tool.uv.dev_dependencies`
|
||||||
workspace
|
let dev_dependencies = workspace
|
||||||
.pyproject_toml
|
.pyproject_toml()
|
||||||
.tool
|
.tool
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
.and_then(|uv| uv.dev_dependencies.as_ref())
|
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||||
.map(|dev| dev.iter())
|
|
||||||
.into_iter()
|
// Then, collect `dependency-groups`
|
||||||
.flatten(),
|
let dependency_groups = workspace
|
||||||
)
|
.pyproject_toml()
|
||||||
} else {
|
.dependency_groups
|
||||||
Either::Left(std::iter::empty())
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
// Merge any overlapping groups.
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
for (name, dependencies) in
|
||||||
|
FlatDependencyGroups::from_dependency_groups(&dependency_groups)?
|
||||||
|
.into_iter()
|
||||||
|
.chain(
|
||||||
|
// Only add the `dev` group if `dev-dependencies` is defined.
|
||||||
|
dev_dependencies.into_iter().map(|requirements| {
|
||||||
|
(DEV_DEPENDENCIES.clone(), requirements.clone())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
match map.entry(name) {
|
||||||
|
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||||
|
entry.insert(dependencies);
|
||||||
|
}
|
||||||
|
std::collections::btree_map::Entry::Occupied(mut entry) => {
|
||||||
|
entry.get_mut().extend(dependencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,7 @@ pub(crate) async fn add(
|
||||||
DependencyType::Optional(_) => {
|
DependencyType::Optional(_) => {
|
||||||
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::Group(_) => {
|
DependencyType::Group(_) => {}
|
||||||
// TODO(charlie): Allow adding to `dependency-groups` in non-`[project]`
|
|
||||||
// targets, per PEP 735.
|
|
||||||
bail!("Project is missing a `[project]` table; add a `[project]` table to use `dependency-groups` dependencies, or run `{}` instead", "uv add --dev".green())
|
|
||||||
}
|
|
||||||
DependencyType::Dev => (),
|
DependencyType::Dev => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -477,9 +473,6 @@ pub(crate) async fn add(
|
||||||
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
|
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
|
||||||
// If the dependency already exists in `dev-dependencies`, use that.
|
// If the dependency already exists in `dev-dependencies`, use that.
|
||||||
DependencyType::Dev
|
DependencyType::Dev
|
||||||
} else if target.as_project().is_some_and(uv_workspace::VirtualProject::is_non_project) {
|
|
||||||
// TODO(charlie): Allow adding to `dependency-groups` in non-`[project]` targets.
|
|
||||||
DependencyType::Dev
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, use `dependency-groups.dev`, unless it would introduce a separate table.
|
// Otherwise, use `dependency-groups.dev`, unless it would introduce a separate table.
|
||||||
match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) {
|
match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) {
|
||||||
|
|
@ -1018,14 +1011,6 @@ enum Target {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Target {
|
impl Target {
|
||||||
/// Returns the [`VirtualProject`] for the target, if it is a project.
|
|
||||||
fn as_project(&self) -> Option<&VirtualProject> {
|
|
||||||
match self {
|
|
||||||
Self::Project(project, _) => Some(project),
|
|
||||||
Self::Script(_, _) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [`Interpreter`] for the target.
|
/// Returns the [`Interpreter`] for the target.
|
||||||
fn interpreter(&self) -> &Interpreter {
|
fn interpreter(&self) -> &Interpreter {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ async fn do_lock(
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
// Collect the requirements, etc.
|
// Collect the requirements, etc.
|
||||||
let requirements = workspace.non_project_requirements().collect::<Vec<_>>();
|
let requirements = workspace.non_project_requirements()?;
|
||||||
let overrides = workspace.overrides().into_iter().collect::<Vec<_>>();
|
let overrides = workspace.overrides().into_iter().collect::<Vec<_>>();
|
||||||
let constraints = workspace.constraints();
|
let constraints = workspace.constraints();
|
||||||
let dev: Vec<_> = workspace
|
let dev: Vec<_> = workspace
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ use uv_resolver::{
|
||||||
};
|
};
|
||||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
|
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||||
use uv_workspace::pyproject::PyProjectToml;
|
use uv_workspace::pyproject::PyProjectToml;
|
||||||
use uv_workspace::Workspace;
|
use uv_workspace::Workspace;
|
||||||
|
|
||||||
|
|
@ -151,6 +152,9 @@ pub(crate) enum ProjectError {
|
||||||
#[error("Failed to update `pyproject.toml`")]
|
#[error("Failed to update `pyproject.toml`")]
|
||||||
PyprojectTomlUpdate,
|
PyprojectTomlUpdate,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
DependencyGroup(#[from] DependencyGroupError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Python(#[from] uv_python::Error),
|
Python(#[from] uv_python::Error),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4396,12 +4396,13 @@ fn add_non_project() -> Result<()> {
|
||||||
}, {
|
}, {
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
pyproject_toml, @r###"
|
pyproject_toml, @r###"
|
||||||
[tool.uv]
|
|
||||||
dev-dependencies = [
|
|
||||||
"iniconfig>=2.0.0",
|
|
||||||
]
|
|
||||||
[tool.uv.workspace]
|
[tool.uv.workspace]
|
||||||
members = []
|
members = []
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"iniconfig>=2.0.0",
|
||||||
|
]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue