mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
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
10 changed files with 328 additions and 239 deletions
|
@ -7,8 +7,8 @@ use uv_configuration::{LowerBound, SourceStrategy};
|
|||
use uv_distribution_types::IndexLocations;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pep508::Pep508Error;
|
||||
use uv_pypi_types::{HashDigest, ResolutionMetadata, VerbatimParsedUrl};
|
||||
use uv_pypi_types::{HashDigest, ResolutionMetadata};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::WorkspaceError;
|
||||
|
||||
pub use crate::metadata::lowering::LoweredRequirement;
|
||||
|
@ -22,39 +22,12 @@ mod requires_dist;
|
|||
pub enum MetadataError {
|
||||
#[error(transparent)]
|
||||
Workspace(#[from] WorkspaceError),
|
||||
#[error(transparent)]
|
||||
DependencyGroup(#[from] DependencyGroupError),
|
||||
#[error("Failed to parse entry: `{0}`")]
|
||||
LoweringError(PackageName, #[source] Box<LoweringError>),
|
||||
#[error("Failed to parse entry in group `{0}`: `{1}`")]
|
||||
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)]
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
use std::collections::BTreeMap;
|
||||
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_distribution_types::IndexLocations;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
|
||||
use uv_pypi_types::VerbatimParsedUrl;
|
||||
use uv_workspace::pyproject::{DependencyGroupSpecifier, ToolUvSources};
|
||||
use uv_workspace::dependency_groups::FlatDependencyGroups;
|
||||
use uv_workspace::pyproject::ToolUvSources;
|
||||
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
|
||||
|
||||
use crate::metadata::{Cycle, LoweredRequirement, MetadataError};
|
||||
use crate::Metadata;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequiresDist {
|
||||
pub name: PackageName,
|
||||
|
@ -121,54 +117,55 @@ impl RequiresDist {
|
|||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Resolve any `include-group` entries in `dependency-groups`.
|
||||
let dependency_groups = resolve_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())),
|
||||
)
|
||||
.map(|(name, requirements)| {
|
||||
let requirements = match source_strategy {
|
||||
SourceStrategy::Enabled => requirements
|
||||
let dependency_groups =
|
||||
FlatDependencyGroups::from_dependency_groups(&dependency_groups)?
|
||||
.into_iter()
|
||||
.chain(
|
||||
// Only add the `dev` group if `dev-dependencies` is defined.
|
||||
dev_dependencies
|
||||
.into_iter()
|
||||
.flat_map(|requirement| {
|
||||
let group_name = name.clone();
|
||||
let requirement_name = requirement.name.clone();
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
project_sources,
|
||||
project_indexes,
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
lower_bound,
|
||||
)
|
||||
.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),
|
||||
)),
|
||||
}
|
||||
.map(|requirements| (DEV_DEPENDENCIES.clone(), requirements.clone())),
|
||||
)
|
||||
.map(|(name, requirements)| {
|
||||
let requirements = match source_strategy {
|
||||
SourceStrategy::Enabled => requirements
|
||||
.into_iter()
|
||||
.flat_map(|requirement| {
|
||||
let group_name = name.clone();
|
||||
let requirement_name = requirement.name.clone();
|
||||
LoweredRequirement::from_requirement(
|
||||
requirement,
|
||||
&metadata.name,
|
||||
project_workspace.project_root(),
|
||||
project_sources,
|
||||
project_indexes,
|
||||
locations,
|
||||
project_workspace.workspace(),
|
||||
lower_bound,
|
||||
)
|
||||
.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<_>, _>>(),
|
||||
SourceStrategy::Disabled => Ok(requirements
|
||||
.into_iter()
|
||||
.map(uv_pypi_types::Requirement::from)
|
||||
.collect()),
|
||||
}?;
|
||||
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
|
||||
name,
|
||||
requirements,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.collect::<Result<Vec<_>, _>>(),
|
||||
SourceStrategy::Disabled => Ok(requirements
|
||||
.into_iter()
|
||||
.map(uv_pypi_types::Requirement::from)
|
||||
.collect()),
|
||||
}?;
|
||||
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
|
||||
name,
|
||||
requirements,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Merge any overlapping groups.
|
||||
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)]
|
||||
mod test {
|
||||
use std::path::Path;
|
||||
|
|
|
@ -14,6 +14,14 @@ use std::sync::{Arc, LazyLock};
|
|||
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
||||
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_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions};
|
||||
use uv_distribution::DistributionDatabase;
|
||||
|
@ -35,17 +43,9 @@ use uv_pypi_types::{
|
|||
ResolverMarkerEnvironment,
|
||||
};
|
||||
use uv_types::{BuildContext, HashStrategy};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
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 tree;
|
||||
|
||||
|
@ -638,8 +638,11 @@ impl Lock {
|
|||
|
||||
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
|
||||
// dependencies in (legacy) non-project workspace roots).
|
||||
let groups = project
|
||||
.groups()
|
||||
.map_err(|err| LockErrorKind::DependencyGroup { err })?;
|
||||
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, &[]) {
|
||||
let root_name = &dependency.name;
|
||||
let root = self
|
||||
|
@ -4135,6 +4138,11 @@ enum LockErrorKind {
|
|||
#[source]
|
||||
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.
|
||||
|
|
150
crates/uv-workspace/src/dependency_groups.rs
Normal file
150
crates/uv-workspace/src/dependency_groups.rs
Normal file
|
@ -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,
|
||||
};
|
||||
|
||||
pub mod dependency_groups;
|
||||
pub mod pyproject;
|
||||
pub mod pyproject_mut;
|
||||
mod workspace;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
//! 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 glob::{glob, GlobError, PatternError};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
@ -14,10 +18,6 @@ use uv_pypi_types::{Requirement, RequirementSource, SupportedEnvironments, Verba
|
|||
use uv_static::EnvVars;
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
|
||||
use crate::pyproject::{
|
||||
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WorkspaceError {
|
||||
// Workspace structure errors.
|
||||
|
@ -305,7 +305,7 @@ impl Workspace {
|
|||
/// `pyproject.toml`.
|
||||
///
|
||||
/// 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
|
||||
.packages
|
||||
.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
|
||||
// include any root-only requirements.
|
||||
Either::Left(std::iter::empty())
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
// Otherwise, return the dev dependencies in the non-project workspace root.
|
||||
Either::Right(
|
||||
self.pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|requirement| {
|
||||
Requirement::from(
|
||||
requirement
|
||||
.clone()
|
||||
.with_origin(RequirementOrigin::Workspace),
|
||||
)
|
||||
}),
|
||||
)
|
||||
// Otherwise, return the dependency groups in the non-project workspace root.
|
||||
// First, collect `tool.uv.dev_dependencies`
|
||||
let dev_dependencies = self
|
||||
.pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||
|
||||
// Then, collect `dependency-groups`
|
||||
let dependency_groups = self
|
||||
.pyproject_toml
|
||||
.dependency_groups
|
||||
.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.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum InstallTarget<'env> {
|
||||
/// A project (which could be a workspace root or member).
|
||||
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,
|
||||
/// only returns a non-empty iterator for virtual workspaces, which can include dev dependencies
|
||||
/// on the virtual root.
|
||||
pub fn group(
|
||||
pub fn groups(
|
||||
&self,
|
||||
name: &GroupName,
|
||||
) -> impl Iterator<Item = &uv_pep508::Requirement<VerbatimParsedUrl>> {
|
||||
) -> Result<
|
||||
BTreeMap<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
DependencyGroupError,
|
||||
> {
|
||||
match self {
|
||||
Self::Project(_) | Self::FrozenMember(..) => {
|
||||
// For projects, dev dependencies are attached to the members.
|
||||
Either::Left(std::iter::empty())
|
||||
}
|
||||
Self::Project(_) | Self::FrozenMember(..) => Ok(BTreeMap::new()),
|
||||
Self::NonProject(workspace) => {
|
||||
// For non-projects, we might have dev dependencies that are attached to the
|
||||
// workspace root (which isn't a member).
|
||||
if name == &*DEV_DEPENDENCIES {
|
||||
Either::Right(
|
||||
workspace
|
||||
.pyproject_toml
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref())
|
||||
.map(|dev| dev.iter())
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
} else {
|
||||
Either::Left(std::iter::empty())
|
||||
// For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies`
|
||||
// that are attached to the workspace root (which isn't a member).
|
||||
|
||||
// First, collect `tool.uv.dev_dependencies`
|
||||
let dev_dependencies = workspace
|
||||
.pyproject_toml()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.dev_dependencies.as_ref());
|
||||
|
||||
// Then, collect `dependency-groups`
|
||||
let dependency_groups = workspace
|
||||
.pyproject_toml()
|
||||
.dependency_groups
|
||||
.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(_) => {
|
||||
bail!("Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead", "uv add --dev".green())
|
||||
}
|
||||
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::Group(_) => {}
|
||||
DependencyType::Dev => (),
|
||||
}
|
||||
}
|
||||
|
@ -477,9 +473,6 @@ pub(crate) async fn add(
|
|||
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
|
||||
// If the dependency already exists in `dev-dependencies`, use that.
|
||||
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 {
|
||||
// Otherwise, use `dependency-groups.dev`, unless it would introduce a separate table.
|
||||
match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) {
|
||||
|
@ -1018,14 +1011,6 @@ enum 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.
|
||||
fn interpreter(&self) -> &Interpreter {
|
||||
match self {
|
||||
|
|
|
@ -299,7 +299,7 @@ async fn do_lock(
|
|||
} = settings;
|
||||
|
||||
// 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 constraints = workspace.constraints();
|
||||
let dev: Vec<_> = workspace
|
||||
|
|
|
@ -36,6 +36,7 @@ use uv_resolver::{
|
|||
};
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::pyproject::PyProjectToml;
|
||||
use uv_workspace::Workspace;
|
||||
|
||||
|
@ -151,6 +152,9 @@ pub(crate) enum ProjectError {
|
|||
#[error("Failed to update `pyproject.toml`")]
|
||||
PyprojectTomlUpdate,
|
||||
|
||||
#[error(transparent)]
|
||||
DependencyGroup(#[from] DependencyGroupError),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] uv_python::Error),
|
||||
|
||||
|
|
|
@ -4396,12 +4396,13 @@ fn add_non_project() -> Result<()> {
|
|||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"iniconfig>=2.0.0",
|
||||
]
|
||||
[tool.uv.workspace]
|
||||
members = []
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"iniconfig>=2.0.0",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue