@@ -43,7 +43,7 @@ use uv_distribution_types::{
4343 UnresolvedRequirementSpecification ,
4444} ;
4545use uv_fs:: { CWD , Simplified } ;
46- use uv_normalize:: { ExtraName , GroupName , PackageName } ;
46+ use uv_normalize:: { ExtraName , PackageName , PipGroupName } ;
4747use uv_requirements_txt:: { RequirementsTxt , RequirementsTxtRequirement } ;
4848use uv_warnings:: warn_user;
4949use uv_workspace:: pyproject:: PyProjectToml ;
@@ -215,7 +215,7 @@ impl RequirementsSpecification {
215215 requirements : & [ RequirementsSource ] ,
216216 constraints : & [ RequirementsSource ] ,
217217 overrides : & [ RequirementsSource ] ,
218- groups : BTreeMap < PathBuf , Vec < GroupName > > ,
218+ groups : Option < & GroupsSpecification > ,
219219 client_builder : & BaseClientBuilder < ' _ > ,
220220 ) -> Result < Self > {
221221 let mut spec = Self :: default ( ) ;
@@ -272,7 +272,7 @@ impl RequirementsSpecification {
272272 "Cannot specify constraints with a `pylock.toml` file"
273273 ) ) ;
274274 }
275- if !groups. is_empty ( ) {
275+ if groups . is_some_and ( |groups| !groups. groups . is_empty ( ) ) {
276276 return Err ( anyhow:: anyhow!(
277277 "Cannot specify groups with a `pylock.toml` file"
278278 ) ) ;
@@ -287,9 +287,24 @@ impl RequirementsSpecification {
287287 }
288288
289289 // pip `--group` flags specify their own sources, which we need to process here
290- if !groups. is_empty ( ) {
290+ if let Some ( groups) = groups {
291+ // First, we collect all groups by their path.
292+ let mut groups_by_path = BTreeMap :: new ( ) ;
293+ for group in & groups. groups {
294+ // If there's no path provided, expect a pyproject.toml in the project-dir
295+ // (Which is typically the current working directory, matching pip's behaviour)
296+ let pyproject_path = group
297+ . path
298+ . clone ( )
299+ . unwrap_or_else ( || groups. root . join ( "pyproject.toml" ) ) ;
300+ groups_by_path
301+ . entry ( pyproject_path)
302+ . or_insert_with ( Vec :: new)
303+ . push ( group. name . clone ( ) ) ;
304+ }
305+
291306 let mut group_specs = BTreeMap :: new ( ) ;
292- for ( path, groups) in groups {
307+ for ( path, groups) in groups_by_path {
293308 let group_spec = DependencyGroups :: from_args (
294309 false ,
295310 false ,
@@ -426,7 +441,7 @@ impl RequirementsSpecification {
426441 requirements : & [ RequirementsSource ] ,
427442 client_builder : & BaseClientBuilder < ' _ > ,
428443 ) -> Result < Self > {
429- Self :: from_sources ( requirements, & [ ] , & [ ] , BTreeMap :: default ( ) , client_builder) . await
444+ Self :: from_sources ( requirements, & [ ] , & [ ] , None , client_builder) . await
430445 }
431446
432447 /// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`].
@@ -485,3 +500,12 @@ impl RequirementsSpecification {
485500 self . requirements . is_empty ( ) && self . source_trees . is_empty ( ) && self . overrides . is_empty ( )
486501 }
487502}
503+
504+ #[ derive( Debug , Default , Clone ) ]
505+ pub struct GroupsSpecification {
506+ /// The path to the project root, relative to which the default `pyproject.toml` file is
507+ /// located.
508+ pub root : PathBuf ,
509+ /// The enabled groups.
510+ pub groups : Vec < PipGroupName > ,
511+ }
0 commit comments