From dd0f69669562520957ef26dfda67e8ec651fbfdf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 25 Oct 2024 14:57:06 -0400 Subject: [PATCH] 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"] ``` --- crates/uv-distribution/src/metadata/mod.rs | 35 +--- .../src/metadata/requires_dist.rs | 180 +++++------------- crates/uv-resolver/src/lock/mod.rs | 28 ++- crates/uv-workspace/src/dependency_groups.rs | 150 +++++++++++++++ crates/uv-workspace/src/lib.rs | 1 + crates/uv-workspace/src/workspace.rs | 141 +++++++++----- crates/uv/src/commands/project/add.rs | 17 +- crates/uv/src/commands/project/lock.rs | 2 +- crates/uv/src/commands/project/mod.rs | 4 + crates/uv/tests/it/edit.rs | 9 +- 10 files changed, 328 insertions(+), 239 deletions(-) create mode 100644 crates/uv-workspace/src/dependency_groups.rs diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index be8762b3d..26b20d5f8 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -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), #[error("Failed to parse entry in group `{0}`: `{1}`")] GroupLoweringError(GroupName, PackageName, #[source] Box), - #[error("Failed to parse entry in group `{0}`: `{1}`")] - GroupParseError( - GroupName, - String, - #[source] Box>, - ), - #[error("Failed to find group `{0}` included by `{1}`")] - GroupNotFound(GroupName, GroupName), - #[error("Detected a cycle in `dependency-groups`: {0}")] - DependencyGroupCycle(Cycle), -} - -/// A cycle in the `dependency-groups` table. -#[derive(Debug)] -pub struct Cycle(Vec); - -/// Display a cycle, e.g., `a -> b -> c -> a`. -impl std::fmt::Display for Cycle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let [first, rest @ ..] = self.0.as_slice() else { - return Ok(()); - }; - write!(f, "`{first}`")?; - for group in rest { - write!(f, " -> `{group}`")?; - } - write!(f, " -> `{first}`")?; - Ok(()) - } } #[derive(Debug, Clone)] diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index e0eddb15c..0f8962e97 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -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::>(); // 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::, _>>(), - SourceStrategy::Disabled => Ok(requirements - .into_iter() - .map(uv_pypi_types::Requirement::from) - .collect()), - }?; - Ok::<(GroupName, Vec), MetadataError>(( - name, - requirements, - )) - }) - .collect::, _>>()?; + .collect::, _>>(), + SourceStrategy::Disabled => Ok(requirements + .into_iter() + .map(uv_pypi_types::Requirement::from) + .collect()), + }?; + Ok::<(GroupName, Vec), MetadataError>(( + name, + requirements, + )) + }) + .collect::, _>>()?; // Merge any overlapping groups. let mut map = BTreeMap::new(); @@ -235,81 +232,6 @@ impl From 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>, -) -> Result>>, MetadataError> { - fn resolve_group<'data>( - resolved: &mut BTreeMap>>, - groups: &'data BTreeMap<&GroupName, &Vec>, - 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::::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; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 35d13cb10..f858a30ad 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -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. diff --git a/crates/uv-workspace/src/dependency_groups.rs b/crates/uv-workspace/src/dependency_groups.rs new file mode 100644 index 000000000..e3e431fd0 --- /dev/null +++ b/crates/uv-workspace/src/dependency_groups.rs @@ -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>>, +); + +impl FlatDependencyGroups { + /// Resolve the dependency groups (which may contain references to other groups) into concrete + /// lists of requirements. + pub fn from_dependency_groups( + groups: &BTreeMap<&GroupName, &Vec>, + ) -> Result { + fn resolve_group<'data>( + resolved: &mut BTreeMap>>, + groups: &'data BTreeMap<&GroupName, &Vec>, + name: &'data GroupName, + parents: &mut Vec<&'data GroupName>, + ) -> Result<(), DependencyGroupError> { + let Some(specifiers) = groups.get(name) else { + // Missing group + let parent_name = parents + .iter() + .last() + .copied() + .expect("parent when group is missing"); + return Err(DependencyGroupError::GroupNotFound( + name.clone(), + parent_name.clone(), + )); + }; + + // "Dependency Group Includes MUST NOT include cycles, and tools SHOULD report an error if they detect a cycle." + if parents.contains(&name) { + return Err(DependencyGroupError::DependencyGroupCycle(Cycle( + parents.iter().copied().cloned().collect(), + ))); + } + + // If we already resolved this group, short-circuit. + if resolved.contains_key(name) { + return Ok(()); + } + + parents.push(name); + let mut requirements = Vec::with_capacity(specifiers.len()); + for specifier in *specifiers { + match specifier { + DependencyGroupSpecifier::Requirement(requirement) => { + match uv_pep508::Requirement::::from_str(requirement) { + Ok(requirement) => requirements.push(requirement), + Err(err) => { + return Err(DependencyGroupError::GroupParseError( + name.clone(), + requirement.clone(), + Box::new(err), + )); + } + } + } + DependencyGroupSpecifier::IncludeGroup { include_group } => { + resolve_group(resolved, groups, include_group, parents)?; + requirements + .extend(resolved.get(include_group).into_iter().flatten().cloned()); + } + DependencyGroupSpecifier::Object(map) => { + 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>> { + self.0.get(group) + } +} + +impl IntoIterator for FlatDependencyGroups { + type Item = (GroupName, Vec>); + type IntoIter = std::collections::btree_map::IntoIter< + GroupName, + Vec>, + >; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Debug, Error)] +pub enum DependencyGroupError { + #[error("Failed to parse entry in group `{0}`: `{1}`")] + GroupParseError( + GroupName, + String, + #[source] Box>, + ), + #[error("Failed to find group `{0}` included by `{1}`")] + GroupNotFound(GroupName, GroupName), + #[error("Detected a cycle in `dependency-groups`: {0}")] + DependencyGroupCycle(Cycle), +} + +/// A cycle in the `dependency-groups` table. +#[derive(Debug)] +pub struct Cycle(Vec); + +/// Display a cycle, e.g., `a -> b -> c -> a`. +impl std::fmt::Display for Cycle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [first, rest @ ..] = self.0.as_slice() else { + return Ok(()); + }; + write!(f, "`{first}`")?; + for group in rest { + write!(f, " -> `{group}`")?; + } + write!(f, " -> `{first}`")?; + Ok(()) + } +} diff --git a/crates/uv-workspace/src/lib.rs b/crates/uv-workspace/src/lib.rs index 83cfd17a2..74bc631d9 100644 --- a/crates/uv-workspace/src/lib.rs +++ b/crates/uv-workspace/src/lib.rs @@ -3,6 +3,7 @@ pub use workspace::{ VirtualProject, Workspace, WorkspaceError, WorkspaceMember, }; +pub mod dependency_groups; pub mod pyproject; pub mod pyproject_mut; mod workspace; diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index ece22fafa..e0e341c5f 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -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 + '_ { + pub fn non_project_requirements(&self) -> Result, 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::>(); + + // 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> { + ) -> Result< + BTreeMap>>, + 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::>(); + + // 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) } } } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index eb860a67e..d469b9325 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -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 { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 0689c6dd7..f32570021 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -299,7 +299,7 @@ async fn do_lock( } = settings; // Collect the requirements, etc. - let requirements = workspace.non_project_requirements().collect::>(); + let requirements = workspace.non_project_requirements()?; let overrides = workspace.overrides().into_iter().collect::>(); let constraints = workspace.constraints(); let dev: Vec<_> = workspace diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index b6f8c53ef..b85029742 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -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), diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 2b59e554a..22e0853a3 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -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", + ] "### ); });