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:
Charlie Marsh 2024-10-25 14:57:06 -04:00 committed by GitHub
parent 4df9ab2b58
commit dd0f696695
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 328 additions and 239 deletions

View File

@ -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)]

View File

@ -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;

View File

@ -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.

View 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(())
}
}

View File

@ -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;

View File

@ -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)
} }
} }
} }

View File

@ -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 {

View File

@ -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

View File

@ -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),

View File

@ -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",
]
"### "###
); );
}); });