@@ -209,6 +209,27 @@ def __init__(self, _type):
209209 )
210210
211211
212+ class DecoratorRegistrationException (DIException ):
213+ """
214+ Exception raised when registering a decorator fails, either because the base type
215+ is not registered or because the decorator class has no parameter matching the
216+ base type.
217+ """
218+
219+ def __init__ (self , base_type , decorator_type ):
220+ if decorator_type is None :
221+ super ().__init__ (
222+ f"Cannot register a decorator for type '{ class_name (base_type )} ': "
223+ f"the type is not registered in the container."
224+ )
225+ else :
226+ super ().__init__ (
227+ f"Cannot register '{ class_name (decorator_type )} ' as a decorator for "
228+ f"'{ class_name (base_type )} ': no __init__ parameter with a type "
229+ f"annotation matching '{ class_name (base_type )} ' was found."
230+ )
231+
232+
212233class ServiceLifeStyle (Enum ):
213234 TRANSIENT = 1
214235 SCOPED = 2
@@ -787,6 +808,143 @@ def __call__(self, context: ResolutionContext):
787808 return FactoryTypeProvider (self .concrete_type , self .factory )
788809
789810
811+ def _get_resolver_lifestyle (resolver ) -> "ServiceLifeStyle" :
812+ """Returns the ServiceLifeStyle of a resolver, defaulting to SINGLETON."""
813+ if isinstance (resolver , (DynamicResolver , FactoryResolver )):
814+ return resolver .life_style
815+ return ServiceLifeStyle .SINGLETON
816+
817+
818+ class DecoratorResolver :
819+ """
820+ Resolver that wraps an existing resolver with a decorator class. The decorator
821+ must have an __init__ parameter whose type annotation matches (or is a supertype
822+ of) the registered base type; that parameter receives the inner service instance.
823+ All other __init__ parameters are resolved normally from the container.
824+ """
825+
826+ __slots__ = (
827+ "_base_type" ,
828+ "_decorator_type" ,
829+ "_inner_resolver" ,
830+ "services" ,
831+ "life_style" ,
832+ )
833+
834+ def __init__ (self , base_type , decorator_type , inner_resolver , services , life_style ):
835+ self ._base_type = base_type
836+ self ._decorator_type = decorator_type
837+ self ._inner_resolver = inner_resolver
838+ self .services = services
839+ self .life_style = life_style
840+
841+ def _get_resolver (self , desired_type , context : ResolutionContext ):
842+ if desired_type in context .resolved :
843+ return context .resolved [desired_type ]
844+ reg = self .services ._map .get (desired_type )
845+ assert (
846+ reg is not None
847+ ), f"A resolver for type { class_name (desired_type )} is not configured"
848+ resolver = reg (context )
849+ context .resolved [desired_type ] = resolver
850+ return resolver
851+
852+ def __call__ (self , context : ResolutionContext ):
853+ inner_provider = self ._inner_resolver (context )
854+
855+ sig = Signature .from_callable (self ._decorator_type .__init__ )
856+ params = {
857+ key : Dependency (key , value .annotation )
858+ for key , value in sig .parameters .items ()
859+ }
860+
861+ globalns = dict (vars (sys .modules [self ._decorator_type .__module__ ]))
862+ globalns .update (_get_obj_globals (self ._decorator_type ))
863+ try :
864+ annotations = get_type_hints (
865+ self ._decorator_type .__init__ ,
866+ globalns ,
867+ _get_obj_locals (self ._decorator_type ),
868+ )
869+ for key , value in params .items ():
870+ if key in annotations :
871+ value .annotation = annotations [key ]
872+ except Exception :
873+ pass
874+
875+ fns = []
876+ decoratee_found = False
877+
878+ for param_name , dep in params .items ():
879+ if param_name in ("self" , "args" , "kwargs" ):
880+ continue
881+
882+ annotation = dep .annotation
883+ if (
884+ annotation is not _empty
885+ and isclass (annotation )
886+ and annotation is not object
887+ and issubclass (self ._base_type , annotation )
888+ ):
889+ fns .append (inner_provider )
890+ decoratee_found = True
891+ else :
892+ if annotation is _empty or annotation not in self .services ._map :
893+ raise CannotResolveParameterException (
894+ param_name , self ._decorator_type
895+ )
896+ fns .append (self ._get_resolver (annotation , context ))
897+
898+ if not decoratee_found :
899+ raise DecoratorRegistrationException (self ._base_type , self ._decorator_type )
900+
901+ # Also resolve class-level annotations (property injection), excluding any
902+ # names already covered by __init__ params or ClassVar / pre-initialised attrs.
903+ init_param_names = set (params .keys ())
904+ annotation_resolvers : dict [str , Callable ] = {}
905+
906+ if self ._decorator_type .__annotations__ :
907+ class_hints = get_type_hints (
908+ self ._decorator_type ,
909+ {
910+ ** dict (vars (sys .modules [self ._decorator_type .__module__ ])),
911+ ** _get_obj_globals (self ._decorator_type ),
912+ },
913+ _get_obj_locals (self ._decorator_type ),
914+ )
915+ for attr_name , attr_type in class_hints .items ():
916+ if attr_name in init_param_names :
917+ continue
918+ is_classvar = getattr (attr_type , "__origin__" , None ) is ClassVar
919+ is_initialized = (
920+ getattr (self ._decorator_type , attr_name , None ) is not None
921+ )
922+ if is_classvar or is_initialized :
923+ continue
924+ if attr_type not in self .services ._map :
925+ raise CannotResolveParameterException (
926+ attr_name , self ._decorator_type
927+ )
928+ annotation_resolvers [attr_name ] = self ._get_resolver (attr_type , context )
929+
930+ decorator_type = self ._decorator_type
931+
932+ if annotation_resolvers :
933+
934+ def factory (context , parent_type ):
935+ instance = decorator_type (* [fn (context , parent_type ) for fn in fns ])
936+ for name , resolver in annotation_resolvers .items ():
937+ setattr (instance , name , resolver (context , parent_type ))
938+ return instance
939+
940+ else :
941+
942+ def factory (context , parent_type ):
943+ return decorator_type (* [fn (context , parent_type ) for fn in fns ])
944+
945+ return FactoryResolver (decorator_type , factory , self .life_style )(context )
946+
947+
790948first_cap_re = re .compile ("(.)([A-Z][a-z]+)" )
791949all_cap_re = re .compile ("([a-z0-9])([A-Z])" )
792950
@@ -1227,6 +1385,38 @@ def add_transient(
12271385
12281386 return self .bind_types (base_type , concrete_type , ServiceLifeStyle .TRANSIENT )
12291387
1388+ def decorate (
1389+ self : _ContainerSelf ,
1390+ base_type : Type ,
1391+ decorator_type : Type ,
1392+ ) -> _ContainerSelf :
1393+ """
1394+ Registers a decorator for an already-registered type. The decorator wraps the
1395+ existing service: when base_type is resolved, the decorator instance is returned
1396+ with the inner service injected as the decorated dependency.
1397+
1398+ The decorator class must have an __init__ parameter whose type annotation is
1399+ base_type (or a supertype of it); that parameter receives the inner service.
1400+ All other __init__ parameters are resolved from the container as usual.
1401+
1402+ Calling decorate() multiple times for the same type chains the decorators —
1403+ each wrapping the previous one (last registered = outermost decorator).
1404+
1405+ :param base_type: the type being decorated (must already be registered)
1406+ :param decorator_type: the decorator class
1407+ :return: the service collection itself
1408+ """
1409+ existing = self ._map .get (base_type )
1410+ if existing is None :
1411+ raise DecoratorRegistrationException (base_type , None )
1412+ life_style = _get_resolver_lifestyle (existing )
1413+ self ._map [base_type ] = DecoratorResolver (
1414+ base_type , decorator_type , existing , self , life_style
1415+ )
1416+ if self ._provider is not None :
1417+ self ._provider = None
1418+ return self
1419+
12301420 def _add_exact_singleton (
12311421 self : _ContainerSelf , concrete_type : Type
12321422 ) -> _ContainerSelf :
0 commit comments