From b9b685e3a5e69316c1870e7572085db91601435e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 31 Oct 2024 16:04:53 -0400 Subject: [PATCH] Avoid error for --group defined in non-root workspace member --- crates/uv/src/commands/project/export.rs | 2 +- crates/uv/src/commands/project/mod.rs | 47 +++++++++++++++++++----- crates/uv/src/commands/project/run.rs | 2 +- crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/src/commands/project/tree.rs | 4 +- crates/uv/tests/it/sync.rs | 26 +++++++++++++ 6 files changed, 69 insertions(+), 14 deletions(-) diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 25d1f45f4640..20fca2bce6da 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -73,7 +73,7 @@ pub(crate) async fn export( }; // Determine the default groups to include. - validate_dependency_groups(project.pyproject_toml(), &dev)?; + validate_dependency_groups(&project, &dev)?; let defaults = default_dependency_groups(project.pyproject_toml())?; let VirtualProject::Project(project) = project else { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 02854f8206cf..d11320c1ac0b 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -38,7 +38,7 @@ 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; +use uv_workspace::{VirtualProject, Workspace}; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::operations::{Changelog, Modifications}; @@ -126,7 +126,10 @@ pub(crate) enum ProjectError { ), #[error("Group `{0}` is not defined in the project's `dependency-group` table")] - MissingGroup(GroupName), + MissingGroupProject(GroupName), + + #[error("Group `{0}` is not defined in any project's `dependency-group` table")] + MissingGroupWorkspace(GroupName), #[error("Default group `{0}` (from `tool.uv.default-groups`) is not defined in the project's `dependency-group` table")] MissingDefaultGroup(GroupName), @@ -1366,7 +1369,7 @@ pub(crate) async fn script_python_requirement( /// Validate the dependency groups requested by the [`DevGroupsSpecification`]. #[allow(clippy::result_large_err)] pub(crate) fn validate_dependency_groups( - pyproject_toml: &PyProjectToml, + project: &VirtualProject, dev: &DevGroupsSpecification, ) -> Result<(), ProjectError> { for group in dev @@ -1374,12 +1377,38 @@ pub(crate) fn validate_dependency_groups( .into_iter() .flat_map(GroupsSpecification::names) { - if !pyproject_toml - .dependency_groups - .as_ref() - .is_some_and(|groups| groups.contains_key(group)) - { - return Err(ProjectError::MissingGroup(group.clone())); + match project { + VirtualProject::Project(project) => { + // The group must be defined in the target project. + if !project + .current_project() + .pyproject_toml() + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + { + return Err(ProjectError::MissingGroupProject(group.clone())); + } + } + VirtualProject::NonProject(workspace) => { + // The group must be defined in at least one workspace package. + if !workspace + .pyproject_toml() + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + { + if workspace.packages().values().all(|package| { + !package + .pyproject_toml() + .dependency_groups + .as_ref() + .is_some_and(|groups| groups.contains_key(group)) + }) { + return Err(ProjectError::MissingGroupWorkspace(group.clone())); + } + } + } } } Ok(()) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 0157a9c60b4a..6f20a4df1621 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -551,7 +551,7 @@ pub(crate) async fn run( } } else { // Determine the default groups to include. - validate_dependency_groups(project.pyproject_toml(), &dev)?; + validate_dependency_groups(&project, &dev)?; let defaults = default_dependency_groups(project.pyproject_toml())?; // Determine the lock mode. diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 418b6b30b847..7070db979936 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -96,7 +96,7 @@ pub(crate) async fn sync( } // Determine the default groups to include. - validate_dependency_groups(project.pyproject_toml(), &dev)?; + validate_dependency_groups(&project, &dev)?; let defaults = default_dependency_groups(project.pyproject_toml())?; // Discover or create the virtual environment. diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index c9ef07ee45c2..c40392402309 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -9,7 +9,7 @@ use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTr use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_resolver::TreeDisplay; -use uv_workspace::{DiscoveryOptions, Workspace}; +use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::resolution_markers; @@ -50,7 +50,7 @@ pub(crate) async fn tree( let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; // Determine the default groups to include. - validate_dependency_groups(workspace.pyproject_toml(), &dev)?; + validate_dependency_groups(&VirtualProject::NonProject(workspace.clone()), &dev)?; let defaults = default_dependency_groups(workspace.pyproject_toml())?; // Find an interpreter for the project, unless `--frozen` and `--universal` are both set. diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 1841722d671b..56b61fb5dc47 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -502,6 +502,9 @@ fn sync_legacy_non_project_group() -> Result<()> { requires-python = ">=3.12" dependencies = ["iniconfig>1"] + [dependency-groups] + baz = ["typing-extensions"] + [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" @@ -559,6 +562,29 @@ fn sync_legacy_non_project_group() -> Result<()> { + typing-extensions==4.10.0 "###); + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 2 packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + + iniconfig==2.0.0 + - typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bop"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Group `bop` is not defined in any project's `dependency-group` table + "###); + Ok(()) }