1+ import contextvars
12import inspect
23import re
34import sys
@@ -259,6 +260,8 @@ def get(
259260 * ,
260261 default : Optional [Any ] = ...,
261262 ) -> T :
263+ if self .provider is None :
264+ raise TypeError ("This scope is disposed." )
262265 return self .provider .get (desired_type , scope or self , default = default )
263266
264267 def dispose (self ):
@@ -270,6 +273,48 @@ def dispose(self):
270273 self .scoped_services = None
271274
272275
276+ class TrackingActivationScope (ActivationScope ):
277+ """
278+ This is an experimental class to support nested scopes transparently.
279+ To use it, create a container including the `scope_cls` parameter:
280+ `Container(scope_cls=TrackingActivationScope)`.
281+ """
282+
283+ _active_scopes = contextvars .ContextVar ("active_scopes" , default = [])
284+
285+ __slots__ = ("scoped_services" , "provider" , "parent_scope" )
286+
287+ def __init__ (self , provider = None , scoped_services = None ):
288+ # Get the current stack of active scopes
289+ stack = self ._active_scopes .get ()
290+
291+ # Detect the parent scope if it exists
292+ self .parent_scope = stack [- 1 ] if stack else None
293+
294+ # Initialize scoped services
295+ scoped_services = scoped_services or {}
296+ if self .parent_scope :
297+ scoped_services .update (self .parent_scope .scoped_services )
298+
299+ super ().__init__ (provider , scoped_services )
300+
301+ def __enter__ (self ):
302+ # Push this scope onto the stack
303+ stack = self ._active_scopes .get ()
304+ self ._active_scopes .set (stack + [self ])
305+ return self
306+
307+ def __exit__ (self , exc_type , exc_val , exc_tb ):
308+ # Pop this scope from the stack
309+ stack = self ._active_scopes .get ()
310+ self ._active_scopes .set (stack [:- 1 ])
311+ self .dispose ()
312+
313+ def dispose (self ):
314+ if self .provider :
315+ self .provider = None
316+
317+
273318class ResolutionContext :
274319 __slots__ = ("resolved" , "dynamic_chain" )
275320 __deletable__ = ("resolved" ,)
@@ -679,13 +724,18 @@ class Services:
679724 Provides methods to activate instances of classes, by cached activator functions.
680725 """
681726
682- __slots__ = ("_map" , "_executors" )
727+ __slots__ = ("_map" , "_executors" , "_scope_cls" )
683728
684- def __init__ (self , services_map = None ):
729+ def __init__ (
730+ self ,
731+ services_map = None ,
732+ scope_cls : Optional [Type [ActivationScope ]] = None ,
733+ ):
685734 if services_map is None :
686735 services_map = {}
687736 self ._map = services_map
688737 self ._executors = {}
738+ self ._scope_cls = scope_cls or ActivationScope
689739
690740 def __contains__ (self , item ):
691741 return item in self ._map
@@ -696,6 +746,11 @@ def __getitem__(self, item):
696746 def __setitem__ (self , key , value ):
697747 self .set (key , value )
698748
749+ def create_scope (
750+ self , scoped : Optional [Dict [Union [Type , str ], Any ]] = None
751+ ) -> ActivationScope :
752+ return self ._scope_cls (self , scoped )
753+
699754 def set (self , new_type : Union [Type , str ], value : Any ):
700755 """
701756 Sets a new service of desired type, as singleton.
@@ -733,7 +788,7 @@ def get(
733788 :return: an instance of the desired type
734789 """
735790 if scope is None :
736- scope = ActivationScope ( self )
791+ scope = self . create_scope ( )
737792
738793 resolver = self ._map .get (desired_type )
739794 scoped_service = scope .scoped_services .get (desired_type ) if scope else None
@@ -781,15 +836,15 @@ def get_executor(self, method: Callable) -> Callable:
781836 if iscoroutinefunction (method ):
782837
783838 async def async_executor (
784- scoped : Optional [Dict [Union [Type , str ], Any ]] = None
839+ scoped : Optional [Dict [Union [Type , str ], Any ]] = None ,
785840 ):
786- with ActivationScope ( self , scoped ) as context :
841+ with self . create_scope ( scoped ) as context :
787842 return await method (* [fn (context ) for fn in fns ])
788843
789844 return async_executor
790845
791846 def executor (scoped : Optional [Dict [Union [Type , str ], Any ]] = None ):
792- with ActivationScope ( self , scoped ) as context :
847+ with self . create_scope ( scoped ) as context :
793848 return method (* [fn (context ) for fn in fns ])
794849
795850 return executor
@@ -842,13 +897,19 @@ class Container(ContainerProtocol):
842897 Configuration class for a collection of services.
843898 """
844899
845- __slots__ = ("_map" , "_aliases" , "_exact_aliases" , "strict" )
900+ __slots__ = ("_map" , "_aliases" , "_exact_aliases" , "_scope_cls" , " strict" )
846901
847- def __init__ (self , * , strict : bool = False ):
902+ def __init__ (
903+ self ,
904+ * ,
905+ strict : bool = False ,
906+ scope_cls : Optional [Type [ActivationScope ]] = None ,
907+ ):
848908 self ._map : Dict [Type , Callable ] = {}
849909 self ._aliases : DefaultDict [str , Set [Type ]] = defaultdict (set )
850910 self ._exact_aliases : Dict [str , Type ] = {}
851911 self ._provider : Optional [Services ] = None
912+ self ._scope_cls = scope_cls
852913 self .strict = strict
853914
854915 @property
@@ -1205,7 +1266,7 @@ def build_provider(self) -> Services:
12051266 for name , _type in self ._exact_aliases .items ():
12061267 _map [name ] = self ._get_alias_target_type (name , _map , _type )
12071268
1208- return Services (_map )
1269+ return Services (_map , scope_cls = self . _scope_cls )
12091270
12101271 @staticmethod
12111272 def _get_alias_target_type (name , _map , _type ):
0 commit comments