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
1415from packaging .licenses import canonicalize_license_expression
1516from packaging .utils import canonicalize_name
1617
18+ from poetry .core .packages .dependency import Dependency
1719from poetry .core .utils .helpers import combine_unicode
1820from poetry .core .utils .helpers import readme_content_type
1921
2022
2123if TYPE_CHECKING :
2224 from packaging .utils import NormalizedName
2325
24- from poetry .core .packages .dependency import Dependency
2526 from poetry .core .packages .dependency_group import DependencyGroup
2627 from poetry .core .packages .project_package import ProjectPackage
2728 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 ,
@@ -394,48 +415,62 @@ def _configure_package_dependencies(
394415 package .extras = package_extras
395416
396417 if "dependencies" in tool_poetry :
397- cls ._add_package_group_dependencies (
418+ cls ._add_package_poetry_group_dependencies (
398419 package = package ,
399420 group = MAIN_GROUP ,
400421 dependencies = tool_poetry ["dependencies" ],
401422 )
402423
403424 if with_groups :
404- normalized_groups = cls ._normalize_dependency_group_names (dependency_groups )
405- included = cls ._resolve_dependency_group_includes (normalized_groups )
406- for group_name , dependencies in normalized_groups .items ():
407- poetry_group_config = tool_poetry .get ("group" , {}).get (group_name , {})
425+ tool_poetry_groups = tool_poetry .get ("group" , {})
426+ tool_poetry_groups_normalized = {
427+ canonicalize_name (name ): config
428+ for name , config in tool_poetry_groups .items ()
429+ }
430+ # create groups from the dependency-groups section considering
431+ # additional information from the corresponding tool.poetry.group section
432+ pep739_include_groups = {}
433+ for group_name , dependencies in dependency_groups .items ():
434+ poetry_group_config = tool_poetry_groups_normalized .get (
435+ canonicalize_name (group_name ), {}
436+ )
408437 group = DependencyGroup (
409438 name = group_name ,
410439 optional = poetry_group_config .get ("optional" , False ),
411440 )
412441 package .add_dependency_group (group )
413-
414- for constraint in dependencies :
415- dep = Dependency .create_from_pep_508 (
416- constraint ,
417- relative_to = package .root_dir ,
418- groups = [group_name ],
419- )
420- group .add_dependency (dep )
421-
422- for group_name , group_config in tool_poetry .get ("group" , {}).items ():
423- if not package .has_dependency_group (group_name ):
442+ included_groups = cls ._add_package_pep735_group_dependencies (
443+ package = package ,
444+ group = group ,
445+ dependencies = dependencies ,
446+ )
447+ pep739_include_groups [group_name ] = included_groups
448+ # create groups from the tool.poetry.group section
449+ # with no corresponding entry in dependency-groups
450+ # and add dependency information for existing groups
451+ poetry_include_groups = {}
452+ for group_name , group_config in tool_poetry_groups .items ():
453+ poetry_include_groups [group_name ] = group_config .get (
454+ "include-groups" , []
455+ )
456+ if package .has_dependency_group (group_name ):
457+ group = package .dependency_group (group_name )
458+ else :
424459 group = DependencyGroup (
425460 name = group_name ,
426461 optional = group_config .get ("optional" , False ),
427462 )
428463 package .add_dependency_group (group )
464+ cls ._add_package_poetry_group_dependencies (
465+ package = package ,
466+ group = group ,
467+ dependencies = group_config .get ("dependencies" , {}),
468+ )
429469
430- for group_name_ in (group_name , * included .get (group_name , [])):
431- cls ._add_package_group_dependencies (
432- package = package ,
433- group = group_name_ ,
434- dependencies = group_config .get ("dependencies" , {}),
435- )
436-
437- for group_name , group_config in tool_poetry .get ("group" , {}).items ():
438- if include_groups := group_config .get ("include-groups" , []):
470+ for group_name , include_groups in chain (
471+ pep739_include_groups .items (), poetry_include_groups .items ()
472+ ):
473+ if include_groups :
439474 current_group = package .dependency_group (group_name )
440475 for name in include_groups :
441476 try :
@@ -451,7 +486,7 @@ def _configure_package_dependencies(
451486 current_group .include_dependency_group (group_to_include )
452487
453488 if with_groups and "dev-dependencies" in tool_poetry :
454- cls ._add_package_group_dependencies (
489+ cls ._add_package_poetry_group_dependencies (
455490 package = package ,
456491 group = "dev" ,
457492 dependencies = tool_poetry ["dev-dependencies" ],
@@ -477,65 +512,6 @@ def _configure_package_dependencies(
477512
478513 package .extras = package_extras
479514
480- @classmethod
481- def _normalize_dependency_group_names (
482- cls ,
483- dependency_groups : dict [str , list [str | dict [str , str ]]],
484- ) -> dict [NormalizedName , list [str | dict [str , str ]]]:
485- original_names = defaultdict (list )
486- normalized_groups : dict [NormalizedName , list [str | dict [str , str ]]] = {}
487-
488- for group_name , value in dependency_groups .items ():
489- normed_group_name = canonicalize_name (group_name )
490- original_names [normed_group_name ].append (group_name )
491- normalized_groups [normed_group_name ] = value
492-
493- errors = []
494- for normed_name , names in original_names .items ():
495- if len (names ) > 1 :
496- errors .append (f"{ normed_name } ({ ', ' .join (names )} )" )
497- if errors :
498- raise ValueError (f"Duplicate dependency group names: { ', ' .join (errors )} " )
499-
500- return normalized_groups
501-
502- @classmethod
503- def _resolve_dependency_group_includes (
504- cls , dependency_groups : dict [NormalizedName , list [str | dict [str , str ]]]
505- ) -> dict [NormalizedName , list [NormalizedName ]]:
506- resolved_groups : set [NormalizedName ] = set ()
507- included : dict [NormalizedName , list [NormalizedName ]] = defaultdict (list )
508- while resolved_groups != set (dependency_groups ):
509- for group , dependencies in dependency_groups .items ():
510- if group in resolved_groups :
511- continue
512- if all (isinstance (dep , str ) for dep in dependencies ):
513- resolved_groups .add (group )
514- continue
515- resolved_dependencies : list [str | dict [str , str ]] = []
516- for dep in dependencies :
517- if isinstance (dep , str ):
518- resolved_dependencies .append (dep )
519- else :
520- included_group = canonicalize_name (dep ["include-group" ])
521- if included_group in included [group ]:
522- raise ValueError (
523- f"Cyclic dependency group include:"
524- f" { group } -> { included_group } "
525- )
526- included [included_group ].append (group )
527- try :
528- resolved_dependencies .extend (
529- dependency_groups [included_group ]
530- )
531- except KeyError :
532- raise ValueError (
533- f"Dependency group '{ included_group } '"
534- f" (included in '{ group } ') not found"
535- )
536- dependency_groups [group ] = resolved_dependencies
537- return included
538-
539515 @classmethod
540516 def _prepare_formats (
541517 cls ,
@@ -781,7 +757,7 @@ def validate(
781757 ' Use "poetry.group.dev.dependencies" instead.'
782758 )
783759
784- cls ._validate_dependency_groups_includes (toml_data , result )
760+ cls ._validate_dependency_groups (toml_data , result )
785761
786762 if strict :
787763 # Validate [project] section
@@ -796,19 +772,41 @@ def validate(
796772 return result
797773
798774 @classmethod
799- def _validate_dependency_groups_includes (
775+ def _validate_dependency_groups (
800776 cls , toml_data : dict [str , Any ], result : dict [str , list [str ]]
801777 ) -> None :
802- """Ensure that dependency groups do not include themselves."""
803- config = toml_data . setdefault ( "tool" , {}). setdefault ( "poetry" , {})
804-
778+ """Ensure that there are no duplicated dependency groups
779+ and that they do not include themselves."""
780+ original_names = defaultdict ( set )
805781 group_includes : dict [NormalizedName , list [NormalizedName ]] = {}
806- for group_name , group_config in config .get ("group" , {}).items ():
782+
783+ for group_name , dependencies in toml_data .get ("dependency-groups" , {}).items ():
784+ normalized_group_name = canonicalize_name (group_name )
785+ original_names [normalized_group_name ].add (group_name )
786+ for constraint in dependencies :
787+ if isinstance (constraint , dict ) and (
788+ include := constraint .get ("include-group" )
789+ ):
790+ group_includes .setdefault (normalized_group_name , []).append (
791+ canonicalize_name (include )
792+ )
793+
794+ poetry_config = toml_data .get ("tool" , {}).get ("poetry" , {})
795+ for group_name , group_config in poetry_config .get ("group" , {}).items ():
796+ normalized_group_name = canonicalize_name (group_name )
797+ original_names [normalized_group_name ].add (group_name )
807798 if include_groups := group_config .get ("include-groups" , []):
808- group_includes [canonicalize_name ( group_name ) ] = [
799+ group_includes [normalized_group_name ] = [
809800 canonicalize_name (name ) for name in include_groups
810801 ]
811802
803+ for normed_name , names in original_names .items ():
804+ if len (names ) > 1 :
805+ result ["errors" ].append (
806+ "Duplicate dependency group name after normalization:"
807+ f" { normed_name } ({ ', ' .join (sorted (names ))} )"
808+ )
809+
812810 for root in group_includes :
813811 # group, path to group, ancestors
814812 stack : list [
0 commit comments