44
55from collections import defaultdict
66from collections .abc import Mapping
7+ from itertools import chain
78from pathlib import Path
89from typing import TYPE_CHECKING
910from typing import Any
1314
1415from packaging .utils import canonicalize_name
1516
17+ from poetry .core .packages .dependency import Dependency
1618from poetry .core .utils .helpers import combine_unicode
1719from poetry .core .utils .helpers import readme_content_type
1820
1921
2022if TYPE_CHECKING :
2123 from packaging .utils import NormalizedName
2224
23- from poetry .core .packages .dependency import Dependency
2425 from poetry .core .packages .dependency_group import DependencyGroup
2526 from poetry .core .packages .project_package import ProjectPackage
2627 from poetry .core .poetry import Poetry
@@ -87,7 +88,27 @@ def get_package(cls, name: str, version: str) -> ProjectPackage:
8788 return ProjectPackage (name , version )
8889
8990 @classmethod
90- def _add_package_group_dependencies (
91+ def _add_package_pep735_group_dependencies (
92+ cls ,
93+ package : ProjectPackage ,
94+ group : DependencyGroup ,
95+ dependencies : list [str | dict [str , str ]],
96+ ) -> list [str ]:
97+ group_includes = []
98+ for constraint in dependencies :
99+ if isinstance (constraint , str ):
100+ dep = Dependency .create_from_pep_508 (
101+ constraint ,
102+ relative_to = package .root_dir ,
103+ groups = [group .pretty_name ],
104+ )
105+ group .add_dependency (dep )
106+ else :
107+ group_includes .append (constraint ["include-group" ])
108+ return group_includes
109+
110+ @classmethod
111+ def _add_package_poetry_group_dependencies (
91112 cls ,
92113 package : ProjectPackage ,
93114 group : str | DependencyGroup ,
@@ -339,48 +360,62 @@ def _configure_package_dependencies(
339360 package .extras = package_extras
340361
341362 if "dependencies" in tool_poetry :
342- cls ._add_package_group_dependencies (
363+ cls ._add_package_poetry_group_dependencies (
343364 package = package ,
344365 group = MAIN_GROUP ,
345366 dependencies = tool_poetry ["dependencies" ],
346367 )
347368
348369 if with_groups :
349- normalized_groups = cls ._normalize_dependency_group_names (dependency_groups )
350- included = cls ._resolve_dependency_group_includes (normalized_groups )
351- for group_name , dependencies in normalized_groups .items ():
352- poetry_group_config = tool_poetry .get ("group" , {}).get (group_name , {})
370+ tool_poetry_groups = tool_poetry .get ("group" , {})
371+ tool_poetry_groups_normalized = {
372+ canonicalize_name (name ): config
373+ for name , config in tool_poetry_groups .items ()
374+ }
375+ # create groups from the dependency-groups section considering
376+ # additional information from the corresponding tool.poetry.group section
377+ pep739_include_groups = {}
378+ for group_name , dependencies in dependency_groups .items ():
379+ poetry_group_config = tool_poetry_groups_normalized .get (
380+ canonicalize_name (group_name ), {}
381+ )
353382 group = DependencyGroup (
354383 name = group_name ,
355384 optional = poetry_group_config .get ("optional" , False ),
356385 )
357386 package .add_dependency_group (group )
358-
359- for constraint in dependencies :
360- dep = Dependency .create_from_pep_508 (
361- constraint ,
362- relative_to = package .root_dir ,
363- groups = [group_name ],
364- )
365- group .add_dependency (dep )
366-
367- for group_name , group_config in tool_poetry .get ("group" , {}).items ():
368- if not package .has_dependency_group (group_name ):
387+ included_groups = cls ._add_package_pep735_group_dependencies (
388+ package = package ,
389+ group = group ,
390+ dependencies = dependencies ,
391+ )
392+ pep739_include_groups [group_name ] = included_groups
393+ # create groups from the tool.poetry.group section
394+ # with no corresponding entry in dependency-groups
395+ # and add dependency information for existing groups
396+ poetry_include_groups = {}
397+ for group_name , group_config in tool_poetry_groups .items ():
398+ poetry_include_groups [group_name ] = group_config .get (
399+ "include-groups" , []
400+ )
401+ if package .has_dependency_group (group_name ):
402+ group = package .dependency_group (group_name )
403+ else :
369404 group = DependencyGroup (
370405 name = group_name ,
371406 optional = group_config .get ("optional" , False ),
372407 )
373408 package .add_dependency_group (group )
409+ cls ._add_package_poetry_group_dependencies (
410+ package = package ,
411+ group = group ,
412+ dependencies = group_config .get ("dependencies" , {}),
413+ )
374414
375- for group_name_ in (group_name , * included .get (group_name , [])):
376- cls ._add_package_group_dependencies (
377- package = package ,
378- group = group_name_ ,
379- dependencies = group_config .get ("dependencies" , {}),
380- )
381-
382- for group_name , group_config in tool_poetry .get ("group" , {}).items ():
383- if include_groups := group_config .get ("include-groups" , []):
415+ for group_name , include_groups in chain (
416+ pep739_include_groups .items (), poetry_include_groups .items ()
417+ ):
418+ if include_groups :
384419 current_group = package .dependency_group (group_name )
385420 for name in include_groups :
386421 try :
@@ -396,7 +431,7 @@ def _configure_package_dependencies(
396431 current_group .include_dependency_group (group_to_include )
397432
398433 if with_groups and "dev-dependencies" in tool_poetry :
399- cls ._add_package_group_dependencies (
434+ cls ._add_package_poetry_group_dependencies (
400435 package = package ,
401436 group = "dev" ,
402437 dependencies = tool_poetry ["dev-dependencies" ],
@@ -422,65 +457,6 @@ def _configure_package_dependencies(
422457
423458 package .extras = package_extras
424459
425- @classmethod
426- def _normalize_dependency_group_names (
427- cls ,
428- dependency_groups : dict [str , list [str | dict [str , str ]]],
429- ) -> dict [NormalizedName , list [str | dict [str , str ]]]:
430- original_names = defaultdict (list )
431- normalized_groups : dict [NormalizedName , list [str | dict [str , str ]]] = {}
432-
433- for group_name , value in dependency_groups .items ():
434- normed_group_name = canonicalize_name (group_name )
435- original_names [normed_group_name ].append (group_name )
436- normalized_groups [normed_group_name ] = value
437-
438- errors = []
439- for normed_name , names in original_names .items ():
440- if len (names ) > 1 :
441- errors .append (f"{ normed_name } ({ ', ' .join (names )} )" )
442- if errors :
443- raise ValueError (f"Duplicate dependency group names: { ', ' .join (errors )} " )
444-
445- return normalized_groups
446-
447- @classmethod
448- def _resolve_dependency_group_includes (
449- cls , dependency_groups : dict [NormalizedName , list [str | dict [str , str ]]]
450- ) -> dict [NormalizedName , list [NormalizedName ]]:
451- resolved_groups : set [NormalizedName ] = set ()
452- included : dict [NormalizedName , list [NormalizedName ]] = defaultdict (list )
453- while resolved_groups != set (dependency_groups ):
454- for group , dependencies in dependency_groups .items ():
455- if group in resolved_groups :
456- continue
457- if all (isinstance (dep , str ) for dep in dependencies ):
458- resolved_groups .add (group )
459- continue
460- resolved_dependencies : list [str | dict [str , str ]] = []
461- for dep in dependencies :
462- if isinstance (dep , str ):
463- resolved_dependencies .append (dep )
464- else :
465- included_group = canonicalize_name (dep ["include-group" ])
466- if included_group in included [group ]:
467- raise ValueError (
468- f"Cyclic dependency group include:"
469- f" { group } -> { included_group } "
470- )
471- included [included_group ].append (group )
472- try :
473- resolved_dependencies .extend (
474- dependency_groups [included_group ]
475- )
476- except KeyError :
477- raise ValueError (
478- f"Dependency group '{ included_group } '"
479- f" (included in '{ group } ') not found"
480- )
481- dependency_groups [group ] = resolved_dependencies
482- return included
483-
484460 @classmethod
485461 def _prepare_formats (
486462 cls ,
@@ -726,7 +702,7 @@ def validate(
726702 ' Use "poetry.group.dev.dependencies" instead.'
727703 )
728704
729- cls ._validate_dependency_groups_includes (toml_data , result )
705+ cls ._validate_dependency_groups (toml_data , result )
730706
731707 if strict :
732708 # Validate relation between [project] and [tool.poetry]
@@ -737,19 +713,41 @@ def validate(
737713 return result
738714
739715 @classmethod
740- def _validate_dependency_groups_includes (
716+ def _validate_dependency_groups (
741717 cls , toml_data : dict [str , Any ], result : dict [str , list [str ]]
742718 ) -> None :
743- """Ensure that dependency groups do not include themselves."""
744- config = toml_data . setdefault ( "tool" , {}). setdefault ( "poetry" , {})
745-
719+ """Ensure that there are no duplicated dependency groups
720+ and that they do not include themselves."""
721+ original_names = defaultdict ( set )
746722 group_includes : dict [NormalizedName , list [NormalizedName ]] = {}
747- for group_name , group_config in config .get ("group" , {}).items ():
723+
724+ for group_name , dependencies in toml_data .get ("dependency-groups" , {}).items ():
725+ normalized_group_name = canonicalize_name (group_name )
726+ original_names [normalized_group_name ].add (group_name )
727+ for constraint in dependencies :
728+ if isinstance (constraint , dict ) and (
729+ include := constraint .get ("include-group" )
730+ ):
731+ group_includes .setdefault (normalized_group_name , []).append (
732+ canonicalize_name (include )
733+ )
734+
735+ poetry_config = toml_data .get ("tool" , {}).get ("poetry" , {})
736+ for group_name , group_config in poetry_config .get ("group" , {}).items ():
737+ normalized_group_name = canonicalize_name (group_name )
738+ original_names [normalized_group_name ].add (group_name )
748739 if include_groups := group_config .get ("include-groups" , []):
749- group_includes [canonicalize_name ( group_name ) ] = [
740+ group_includes [normalized_group_name ] = [
750741 canonicalize_name (name ) for name in include_groups
751742 ]
752743
744+ for normed_name , names in original_names .items ():
745+ if len (names ) > 1 :
746+ result ["errors" ].append (
747+ "Duplicate dependency group name after normalization:"
748+ f" { normed_name } ({ ', ' .join (sorted (names ))} )"
749+ )
750+
753751 for root in group_includes :
754752 # group, path to group, ancestors
755753 stack : list [
0 commit comments