@@ -47,45 +47,71 @@ def resolve_dependencies(
4747 self ,
4848 endpoint : Callable [..., Any ],
4949 request_data : RequestData ,
50+ method : str | None = None ,
5051 ) -> dict [str , Any ]:
5152 """
5253 Resolve all dependencies for an endpoint
5354
55+ Generator dependencies stay open so the endpoint can use the
56+ yielded value; the adapter must call ``close(request_data)``
57+ after the endpoint returns to run their cleanup code.
58+
5459 Args:
5560 endpoint: The endpoint function
5661 request_data: Request data container
62+ method: HTTP method of the current request
5763
5864 Returns:
5965 Dict mapping parameter names to resolved dependency values
6066 """
61- # Initialize request-scoped tracking
62- is_top_level = False
67+ self ._open_request_scope (request_data , method )
68+ return self ._resolve_endpoint_dependencies (endpoint , request_data )
69+
70+ def _open_request_scope (
71+ self , request_data : RequestData , method : str | None
72+ ) -> None :
73+ """Create the request-scoped cache entry if it does not exist yet"""
6374 with self ._request_cache_lock :
6475 if request_data not in self ._request_cache :
65- is_top_level = True
6676 self ._request_cache [request_data ] = {
6777 "resolved" : {},
6878 "resolving" : set (),
6979 "generators" : [],
80+ "method" : method ,
7081 }
7182
72- try :
73- return self ._resolve_endpoint_dependencies (endpoint , request_data )
74- finally :
75- if is_top_level :
76- # Get generators before deleting cache
77- with self ._request_cache_lock :
78- cache = self ._request_cache .get (request_data , {})
79- generators = list (cache .get ("generators" , []))
80- # Close generators (triggers finally blocks)
81- for gen in generators :
82- try :
83- gen .close ()
84- except Exception :
85- pass
86- # Clean up request cache
87- with self ._request_cache_lock :
88- self ._request_cache .pop (request_data , None )
83+ def close (self , request_data : RequestData ) -> None :
84+ """
85+ Close generator dependencies opened for a request
86+
87+ Runs code after ``yield`` (via ``gen.close()``) in reverse creation
88+ order and drops the request cache entry. No-op when the request has
89+ no cache entry.
90+ """
91+ with self ._request_cache_lock :
92+ cache = self ._request_cache .pop (request_data , None )
93+ if cache is None :
94+ return
95+ for gen in reversed (cache ["generators" ]):
96+ try :
97+ gen .close ()
98+ except Exception :
99+ pass
100+
101+ async def aclose (self , request_data : RequestData ) -> None :
102+ """Async variant of ``close`` (also handles async generators)"""
103+ with self ._request_cache_lock :
104+ cache = self ._request_cache .pop (request_data , None )
105+ if cache is None :
106+ return
107+ for gen in reversed (cache ["generators" ]):
108+ try :
109+ if inspect .isasyncgen (gen ):
110+ await gen .aclose ()
111+ else :
112+ gen .close ()
113+ except Exception :
114+ pass
89115
90116 def _resolve_endpoint_dependencies (
91117 self , endpoint : Callable [..., Any ], request_data : RequestData
@@ -181,7 +207,7 @@ def _execute_dependency_function(
181207 """
182208 Execute dependency function with caching and circular dependency detection
183209 """
184- cache_key = self ._make_cache_key (dependency_func , request_data )
210+ cache_key = self ._make_cache_key (dependency_func , request_data , security_scopes )
185211 request_cache = self ._get_request_cache (request_data )
186212
187213 # The cache is request-scoped and a request is handled by a single
@@ -306,19 +332,25 @@ def _resolve_sub_dependencies(
306332
307333 return sub_dependencies
308334
309- @staticmethod
310335 def _resolve_regular_params (
336+ self ,
311337 dependency_func : Callable [..., Any ],
312338 regular_params : dict [str , inspect .Parameter ],
313339 request_data : RequestData ,
314340 ) -> dict [str , Any ]:
315341 """Resolve non-dependency parameters of a dependency function"""
316342 from fastopenapi .resolution .resolver import ParameterResolver
317343
344+ # Dependency params follow the same method-specific rules as
345+ # endpoint params (e.g. bare models map to query on GET)
346+ cache = self ._request_cache .get (request_data )
347+ method = cache .get ("method" ) if cache else None
348+
318349 try :
319350 return ParameterResolver .resolve_params (
320351 regular_params ,
321352 request_data ,
353+ method = method ,
322354 owner = (
323355 getattr (dependency_func , "__module__" , "fastopenapi" ),
324356 getattr (dependency_func , "__qualname__" , repr (dependency_func )),
@@ -349,50 +381,25 @@ async def resolve_dependencies_async(
349381 self ,
350382 endpoint : Callable [..., Any ],
351383 request_data : RequestData ,
384+ method : str | None = None ,
352385 ) -> dict [str , Any ]:
353386 """
354387 Resolve all dependencies for an endpoint (async version)
355388
389+ Generator dependencies stay open so the endpoint can use the
390+ yielded value; the adapter must call ``aclose(request_data)``
391+ after the endpoint returns to run their cleanup code.
392+
356393 Args:
357394 endpoint: The endpoint function
358395 request_data: Request data container
396+ method: HTTP method of the current request
359397
360398 Returns:
361399 Dict mapping parameter names to resolved dependency values
362400 """
363- # Initialize request-scoped tracking
364- is_top_level = False
365- with self ._request_cache_lock :
366- if request_data not in self ._request_cache :
367- is_top_level = True
368- self ._request_cache [request_data ] = {
369- "resolved" : {},
370- "resolving" : set (),
371- "generators" : [],
372- }
373-
374- try :
375- return await self ._resolve_endpoint_dependencies_async (
376- endpoint , request_data
377- )
378- finally :
379- if is_top_level :
380- # Get generators before deleting cache
381- with self ._request_cache_lock :
382- cache = self ._request_cache .get (request_data , {})
383- generators = list (cache .get ("generators" , []))
384- # Close generators (triggers finally blocks)
385- for gen in generators :
386- try :
387- if inspect .isasyncgen (gen ):
388- await gen .aclose ()
389- else :
390- gen .close ()
391- except Exception :
392- pass
393- # Clean up request cache
394- with self ._request_cache_lock :
395- self ._request_cache .pop (request_data , None )
401+ self ._open_request_scope (request_data , method )
402+ return await self ._resolve_endpoint_dependencies_async (endpoint , request_data )
396403
397404 async def _resolve_endpoint_dependencies_async (
398405 self , endpoint : Callable [..., Any ], request_data : RequestData
@@ -478,7 +485,7 @@ async def _execute_dependency_function_async(
478485 """
479486 Execute dependency function with caching and circular dependency detection
480487 """
481- cache_key = self ._make_cache_key (dependency_func , request_data )
488+ cache_key = self ._make_cache_key (dependency_func , request_data , security_scopes )
482489 request_cache = self ._get_request_cache (request_data )
483490
484491 hit , value = self ._try_get_cached (cache_key , request_cache )
@@ -573,17 +580,25 @@ def _get_dependency_func(
573580 return dependency_func
574581
575582 def _make_cache_key (
576- self , dependency_func : Callable [..., Any ], request_data : RequestData
577- ) -> tuple [int , int ]:
578- """Create cache key for request-scoped cache"""
579- return (id (dependency_func ), id (request_data ))
583+ self ,
584+ dependency_func : Callable [..., Any ],
585+ request_data : RequestData ,
586+ security_scopes : SecurityScopes | None = None ,
587+ ) -> tuple [int , int , tuple [str , ...]]:
588+ """Create cache key for request-scoped cache
589+
590+ Scopes are part of the key: the same Security dependency requested
591+ with different scopes must be executed once per scope set.
592+ """
593+ scopes = tuple (sorted (security_scopes .scopes )) if security_scopes else ()
594+ return (id (dependency_func ), id (request_data ), scopes )
580595
581596 def _get_request_cache (self , request_data : RequestData ) -> dict [str , Any ]:
582597 """Get cache dictionary for current request"""
583598 return self ._request_cache [request_data ]
584599
585600 def _try_get_cached (
586- self , cache_key : tuple [int , int ], request_cache : dict [str , Any ]
601+ self , cache_key : tuple [int , int , tuple [ str , ...] ], request_cache : dict [str , Any ]
587602 ) -> tuple [bool , Any ]:
588603 """Try to get cached value from request-scoped cache"""
589604 with self ._request_cache_lock :
@@ -593,7 +608,10 @@ def _try_get_cached(
593608 return False , None
594609
595610 def _cache_result (
596- self , cache_key : tuple [int , int ], result : Any , request_cache : dict [str , Any ]
611+ self ,
612+ cache_key : tuple [int , int , tuple [str , ...]],
613+ result : Any ,
614+ request_cache : dict [str , Any ],
597615 ) -> None :
598616 """Store result in request-scoped cache"""
599617 with self ._request_cache_lock :
@@ -647,17 +665,23 @@ def get_cache_stats(self) -> dict[str, int]:
647665
648666# Convenience functions
649667def resolve_dependencies (
650- endpoint : Callable [..., Any ], request_data : RequestData
668+ endpoint : Callable [..., Any ],
669+ request_data : RequestData ,
670+ method : str | None = None ,
651671) -> dict [str , Any ]:
652672 """Convenience function to resolve dependencies (sync)"""
653- return dependency_resolver .resolve_dependencies (endpoint , request_data )
673+ return dependency_resolver .resolve_dependencies (endpoint , request_data , method )
654674
655675
656676async def resolve_dependencies_async (
657- endpoint : Callable [..., Any ], request_data : RequestData
677+ endpoint : Callable [..., Any ],
678+ request_data : RequestData ,
679+ method : str | None = None ,
658680) -> dict [str , Any ]:
659681 """Convenience function to resolve dependencies (async)"""
660- return await dependency_resolver .resolve_dependencies_async (endpoint , request_data )
682+ return await dependency_resolver .resolve_dependencies_async (
683+ endpoint , request_data , method
684+ )
661685
662686
663687def get_dependency_stats () -> dict [str , int ]:
0 commit comments