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

View file

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

View file

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

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,
};
pub mod dependency_groups;
pub mod pyproject;
pub mod pyproject_mut;
mod workspace;

View file

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

View file

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

View file

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

View file

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

View file

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