@@ -4,7 +4,7 @@ use pep508_rs::Requirement;
44use thiserror:: Error ;
55
66/// Resolves a single dependency group or extra.
7- pub fn resolve_group < ' a , T : DependencyEntry > (
7+ fn resolve_group < ' a , T : DependencyEntry > (
88 groups : & ' a IndexMap < String , Vec < T > > ,
99 group : & ' a str ,
1010 resolved : & mut IndexMap < String , Vec < Requirement > > ,
@@ -15,14 +15,15 @@ pub fn resolve_group<'a, T: DependencyEntry>(
1515 if resolved. get ( group) . is_some ( ) {
1616 return Ok ( ( ) ) ;
1717 }
18+ // If the group does not exist in groups, return an error.
1819 let Some ( items) = groups. get ( group) else {
19- // If the group included in another group does not exist, return an error
20- let parent = parents . iter ( ) . last ( ) . expect ( "should have a parent" ) ;
21- return Err ( RecursionResolutionError :: GroupNotFound (
22- T :: group_name ( ) ,
23- group . to_string ( ) ,
24- parent . to_string ( ) ,
25- ) ) ;
20+ let parent = parents . iter ( ) . last ( ) . map ( |s| s . to_string ( ) ) ;
21+ return Err ( GroupNotFound {
22+ group_name : T :: group_name ( ) ,
23+ group : group . to_string ( ) ,
24+ parent ,
25+ }
26+ . into ( ) ) ;
2627 } ;
2728 // If there is a cycle in dependency groups, return an error
2829 if parents. contains ( & group) {
@@ -55,8 +56,8 @@ pub fn resolve_group<'a, T: DependencyEntry>(
5556
5657#[ derive( Debug , Error ) ]
5758pub enum RecursionResolutionError {
58- #[ error( "Failed to find {0} `{1}` included by `{2}`" ) ]
59- GroupNotFound ( String , String , String ) ,
59+ #[ error( transparent ) ]
60+ GroupNotFound ( # [ from ] GroupNotFound ) ,
6061 #[ error( "Detected a cycle in `{0}`: {1}" ) ]
6162 DependencyGroupCycle ( String , Cycle ) ,
6263 #[ error(
@@ -65,6 +66,28 @@ pub enum RecursionResolutionError {
6566 NameCollision ( String ) ,
6667}
6768
69+ #[ derive( Debug , Error ) ]
70+ pub struct GroupNotFound {
71+ group_name : String ,
72+ group : String ,
73+ parent : Option < String > ,
74+ }
75+
76+ impl std:: fmt:: Display for GroupNotFound {
77+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
78+ let Self {
79+ group_name,
80+ group,
81+ parent,
82+ } = self ;
83+ write ! ( f, "Failed to find {group_name} `{group}`" ) ?;
84+ if let Some ( parent) = parent {
85+ write ! ( f, " included by `{parent}`" ) ?;
86+ }
87+ Ok ( ( ) )
88+ }
89+ }
90+
6891/// A cycle in the recursion.
6992#[ derive( Debug ) ]
7093pub struct Cycle ( Vec < String > ) ;
@@ -85,7 +108,7 @@ impl std::fmt::Display for Cycle {
85108}
86109
87110/// A trait that defines how to parse a recursion item.
88- pub trait DependencyEntry {
111+ trait DependencyEntry {
89112 /// Parse the item into a requirement or a reference to other groups.
90113 fn parse < ' a > ( & ' a self , name : Option < & str > ) -> Item < ' a > ;
91114 /// The name of the group in the TOML file.
@@ -94,7 +117,7 @@ pub trait DependencyEntry {
94117 fn table_name ( ) -> String ;
95118}
96119
97- pub enum Item < ' a > {
120+ enum Item < ' a > {
98121 Requirement ( Requirement ) ,
99122 Groups ( Vec < & ' a str > ) ,
100123}
@@ -216,10 +239,11 @@ impl PyProjectToml {
216239
217240#[ cfg( test) ]
218241mod tests {
242+ use indexmap:: IndexMap ;
219243 use pep508_rs:: Requirement ;
220244 use std:: str:: FromStr ;
221245
222- use crate :: PyProjectToml ;
246+ use crate :: { resolution :: resolve_group , PyProjectToml } ;
223247
224248 #[ test]
225249 fn test_parse_pyproject_toml_optional_dependencies_resolve ( ) {
@@ -279,6 +303,37 @@ mod tests {
279303 )
280304 }
281305
306+ #[ test]
307+ fn test_parse_pyproject_toml_optional_dependencies_missing_top_level ( ) {
308+ let source = r#"
309+ [project]
310+ name = "spam"
311+
312+ [project.optional-dependencies]
313+ alpha = ["beta"]
314+ "# ;
315+ let project_toml = PyProjectToml :: new ( source) . unwrap ( ) ;
316+ let mut resolved = IndexMap :: new ( ) ;
317+ let err = resolve_group (
318+ project_toml
319+ . project
320+ . as_ref ( )
321+ . unwrap ( )
322+ . optional_dependencies
323+ . as_ref ( )
324+ . unwrap ( ) ,
325+ "foo" ,
326+ & mut resolved,
327+ & mut Vec :: new ( ) ,
328+ Some ( "spam" ) ,
329+ )
330+ . unwrap_err ( ) ;
331+ assert_eq ! (
332+ err. to_string( ) ,
333+ "Failed to find optional dependency group `foo`"
334+ ) ;
335+ }
336+
282337 #[ test]
283338 fn test_parse_pyproject_toml_dependency_groups_resolve ( ) {
284339 let source = r#"
0 commit comments