@@ -4,35 +4,49 @@ use pep508_rs::Requirement;
44use crate :: has_recursion:: { HasRecursion , Item , RecursionItem , RecursionResolutionError } ;
55use crate :: { DependencyGroupSpecifier , DependencyGroups , OptionalDependencies , PyProjectToml } ;
66
7+ type ResolvedDependencies = IndexMap < String , Vec < Requirement > > ;
8+
79impl PyProjectToml {
810 /// Resolve the optional dependencies and dependency groups into flat lists of requirements.
911 ///
1012 /// This function will recursively resolve all optional dependency groups and dependency groups,
11- /// including those that reference other groups. It will return an error if there is a cycle
12- /// in the groups or if a group references another group that does not exist.
13+ /// including those that reference other groups. It will return an error if
14+ /// - there is a cycle in the groups,
15+ /// - a group references another group that does not exist, or
16+ /// - there is a name collision between optional dependencies and dependency groups.
1317 pub fn resolve (
1418 & self ,
1519 ) -> Result <
16- (
17- Option < IndexMap < String , Vec < Requirement > > > ,
18- Option < IndexMap < String , Vec < Requirement > > > ,
19- ) ,
20+ ( Option < ResolvedDependencies > , Option < ResolvedDependencies > ) ,
2021 RecursionResolutionError ,
2122 > {
2223 let self_reference_name = self . project . as_ref ( ) . map ( |p| p. name . as_str ( ) ) ;
23-
24- // Resolve optional dependencies first, as they may be referenced by dependency groups.
25- let resolved_optional_dependencies = self
24+ let optional_dependencies = self
2625 . project
2726 . as_ref ( )
28- . and_then ( |project| project. optional_dependencies . as_ref ( ) )
27+ . and_then ( |p| p. optional_dependencies . as_ref ( ) ) ;
28+
29+ // Check for collisions between optional dependencies and dependency groups.
30+ if let ( Some ( optional_dependencies) , Some ( dependency_groups) ) =
31+ ( optional_dependencies, self . dependency_groups . as_ref ( ) )
32+ {
33+ for group in optional_dependencies. keys ( ) {
34+ if dependency_groups. contains_key ( group) {
35+ return Err ( RecursionResolutionError :: NameCollision ( group. clone ( ) ) ) ;
36+ }
37+ }
38+ }
39+
40+ // Resolve optional dependencies first, as they may be referenced by dependency groups.
41+ let resolved_optional_dependencies = optional_dependencies
2942 . map ( |optional_dependencies| {
3043 optional_dependencies. resolve_all ( self_reference_name, None )
3144 } )
3245 . transpose ( ) ?;
3346
3447 // Resolve dependency groups, which may reference optional dependencies.
35- let resolved_dependency_groups = self
48+ // At this stage, dependency_groups will contain optional dependency groups as well.
49+ let mut resolved_dependency_groups = self
3650 . dependency_groups
3751 . as_ref ( )
3852 . map ( |dependency_groups| {
@@ -41,6 +55,15 @@ impl PyProjectToml {
4155 } )
4256 . transpose ( ) ?;
4357
58+ // Remove optional dependency groups from the resolved dependency groups.
59+ if let ( Some ( resolved_groups) , Some ( optional_dependencies) ) =
60+ ( resolved_dependency_groups. as_mut ( ) , optional_dependencies)
61+ {
62+ for key in optional_dependencies. keys ( ) {
63+ resolved_groups. shift_remove ( key) ;
64+ }
65+ }
66+
4467 Ok ( ( resolved_optional_dependencies, resolved_dependency_groups) )
4568 }
4669}
@@ -223,4 +246,41 @@ dev = ["spam[test]"]
223246 vec![ Requirement :: from_str( "pytest" ) . unwrap( ) ]
224247 ) ;
225248 }
249+
250+ #[ test]
251+ fn test_name_collision ( ) {
252+ let source = r#"[project]
253+ name = "spam"
254+
255+ [project.optional-dependencies]
256+ dev = ["pytest"]
257+
258+ [dependency-groups]
259+ dev = ["ruff"]
260+ "# ;
261+ let project_toml = PyProjectToml :: new ( source) . unwrap ( ) ;
262+ let err = project_toml. resolve ( ) . unwrap_err ( ) ;
263+ assert_eq ! (
264+ err. to_string( ) ,
265+ "Group `dev` is defined in both `project.optional-dependencies` and `dependency-groups`"
266+ ) ;
267+ }
268+
269+ #[ test]
270+ fn test_optional_dependencies_are_not_dependency_groups ( ) {
271+ let source = r#"[project]
272+ name = "spam"
273+
274+ [project.optional-dependencies]
275+ test = ["pytest"]
276+
277+ [dependency-groups]
278+ dev = ["spam[test]"]
279+ "# ;
280+ let project_toml = PyProjectToml :: new ( source) . unwrap ( ) ;
281+ let ( optional_dependencies, dependency_groups) = project_toml. resolve ( ) . unwrap ( ) ;
282+ assert ! ( optional_dependencies. unwrap( ) . contains_key( "test" ) ) ;
283+ assert ! ( !dependency_groups. as_ref( ) . unwrap( ) . contains_key( "test" ) ) ;
284+ assert ! ( dependency_groups. unwrap( ) . contains_key( "dev" ) ) ;
285+ }
226286}
0 commit comments