diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index e73633331..9ad36d08b 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -132,7 +132,8 @@ impl RequiresDist { // Resolve any `include-group` entries in `dependency-groups`. let dependency_groups = - FlatDependencyGroups::from_dependency_groups(&dependency_groups)? + FlatDependencyGroups::from_dependency_groups(&dependency_groups) + .map_err(|err| err.with_dev_dependencies(dev_dependencies))? .into_iter() .chain( // Only add the `dev` group if `dev-dependencies` is defined. diff --git a/crates/uv-resolver/src/lock/target.rs b/crates/uv-resolver/src/lock/target.rs index 46466c7f7..c5b7b10f5 100644 --- a/crates/uv-resolver/src/lock/target.rs +++ b/crates/uv-resolver/src/lock/target.rs @@ -114,7 +114,8 @@ impl<'env> InstallTarget<'env> { // Merge any overlapping groups. let mut map = BTreeMap::new(); for (name, dependencies) in - FlatDependencyGroups::from_dependency_groups(&dependency_groups)? + FlatDependencyGroups::from_dependency_groups(&dependency_groups) + .map_err(|err| err.with_dev_dependencies(dev_dependencies))? .into_iter() .chain( // Only add the `dev` group if `dev-dependencies` is defined. diff --git a/crates/uv-workspace/src/dependency_groups.rs b/crates/uv-workspace/src/dependency_groups.rs index e3e431fd0..f24667771 100644 --- a/crates/uv-workspace/src/dependency_groups.rs +++ b/crates/uv-workspace/src/dependency_groups.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use thiserror::Error; use tracing::warn; -use uv_normalize::GroupName; +use uv_normalize::{GroupName, DEV_DEPENDENCIES}; use uv_pep508::Pep508Error; use uv_pypi_types::VerbatimParsedUrl; @@ -126,10 +126,30 @@ pub enum DependencyGroupError { ), #[error("Failed to find group `{0}` included by `{1}`")] GroupNotFound(GroupName, GroupName), + #[error("Group `{0}` includes the `dev` group (`include = \"dev\"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead.")] + DevGroupInclude(GroupName), #[error("Detected a cycle in `dependency-groups`: {0}")] DependencyGroupCycle(Cycle), } +impl DependencyGroupError { + /// Enrich a [`DependencyGroupError`] with the `tool.uv.dev-dependencies` metadata, if applicable. + #[must_use] + pub fn with_dev_dependencies( + self, + dev_dependencies: Option<&Vec>>, + ) -> Self { + match self { + DependencyGroupError::GroupNotFound(group, parent) + if dev_dependencies.is_some() && group == *DEV_DEPENDENCIES => + { + DependencyGroupError::DevGroupInclude(parent) + } + _ => self, + } + } +} + /// A cycle in the `dependency-groups` table. #[derive(Debug)] pub struct Cycle(Vec); diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index d22fb7730..bb7107da0 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -343,7 +343,8 @@ impl Workspace { // Resolve any `include-group` entries in `dependency-groups`. let dependency_groups = - FlatDependencyGroups::from_dependency_groups(&dependency_groups)?; + FlatDependencyGroups::from_dependency_groups(&dependency_groups) + .map_err(|err| err.with_dev_dependencies(dev_dependencies))?; // Concatenate the two sets of requirements. let dev_dependencies = dependency_groups diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index b640383ed..78c6de2f6 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -19096,6 +19096,40 @@ fn lock_group_include_cycle() -> Result<()> { Ok(()) } +#[test] +fn lock_group_include_dev() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [tool.uv] + dev-dependencies = ["anyio"] + + [dependency-groups] + foo = ["typing-extensions", {include-group = "dev"}] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to build `project @ file://[TEMP_DIR]/` + ╰─▶ Group `foo` includes the `dev` group (`include = "dev"`), but only `tool.uv.dev-dependencies` was found. To reference the `dev` group via an `include`, remove the `tool.uv.dev-dependencies` section and add any development dependencies to the `dev` entry in the `[dependency-groups]` table instead. + "###); + + Ok(()) +} + #[test] fn lock_group_include_missing() -> Result<()> { let context = TestContext::new("3.12");