aspyx 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aspyx might be problematic. Click here for more details.

aspyx/di/di.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- The deoendency injection module provides a framework for managing dependencies and lifecycle of objects in Python applications.
2
+ The dependency injection module provides a framework for managing dependencies and lifecycle of objects in Python applications.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
@@ -7,9 +7,10 @@ import inspect
7
7
  import logging
8
8
 
9
9
  from abc import abstractmethod, ABC
10
- from enum import Enum, auto
10
+ from enum import Enum
11
11
  import threading
12
- from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable
12
+ from operator import truediv
13
+ from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable, TypedDict
13
14
 
14
15
  from aspyx.di.util import StringBuilder
15
16
  from aspyx.reflection import Decorators, TypeDescriptor, DecoratorDescriptor
@@ -27,10 +28,35 @@ class Factory(ABC, Generic[T]):
27
28
  def create(self) -> T:
28
29
  pass
29
30
 
30
- class InjectorException(Exception):
31
+ class DIException(Exception):
31
32
  """
32
33
  Exception raised for errors in the injector.
33
34
  """
35
+ def __init__(self, message: str):
36
+ super().__init__(message)
37
+
38
+ class DIRegistrationException(DIException):
39
+ """
40
+ Exception raised during the registration of dependencies.
41
+ """
42
+ def __init__(self, message: str):
43
+ super().__init__(message)
44
+
45
+ class ProviderCollisionException(DIRegistrationException):
46
+ def __init__(self, message: str, *providers: AbstractInstanceProvider):
47
+ super().__init__(message)
48
+
49
+ self.providers = providers
50
+
51
+ def __str__(self):
52
+ return f"[{self.args[0]} {self.providers[1].location()} collides with {self.providers[0].location()}"
53
+
54
+ class DIRuntimeException(DIException):
55
+ """
56
+ Exception raised during the runtime.
57
+ """
58
+ def __init__(self, message: str):
59
+ super().__init__(message)
34
60
 
35
61
  class AbstractInstanceProvider(ABC, Generic[T]):
36
62
  """
@@ -40,6 +66,9 @@ class AbstractInstanceProvider(ABC, Generic[T]):
40
66
  def get_module(self) -> str:
41
67
  pass
42
68
 
69
+ def get_host(self) -> Type[T]:
70
+ return type(self)
71
+
43
72
  @abstractmethod
44
73
  def get_type(self) -> Type[T]:
45
74
  pass
@@ -52,16 +81,25 @@ class AbstractInstanceProvider(ABC, Generic[T]):
52
81
  def get_scope(self) -> str:
53
82
  pass
54
83
 
55
- @abstractmethod
56
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
57
- pass
84
+ def get_dependencies(self) -> (list[Type],int):
85
+ return [],1
58
86
 
59
87
  @abstractmethod
60
88
  def create(self, environment: Environment, *args):
61
89
  pass
62
90
 
63
- @abstractmethod
64
- def resolve(self, context: Providers.Context):
91
+ def report(self) -> str:
92
+ return str(self)
93
+
94
+ def location(self) -> str:
95
+ host = self.get_host()
96
+
97
+ file = inspect.getfile(host)
98
+ line = inspect.getsourcelines(host)[1]
99
+
100
+ return f"{file}:{line}"
101
+
102
+ def check_factories(self):
65
103
  pass
66
104
 
67
105
 
@@ -73,8 +111,7 @@ class InstanceProvider(AbstractInstanceProvider):
73
111
  "host",
74
112
  "type",
75
113
  "eager",
76
- "scope",
77
- "dependencies"
114
+ "scope"
78
115
  ]
79
116
 
80
117
  # constructor
@@ -84,16 +121,13 @@ class InstanceProvider(AbstractInstanceProvider):
84
121
  self.type = t
85
122
  self.eager = eager
86
123
  self.scope = scope
87
- self.dependencies : Optional[list[AbstractInstanceProvider]] = None
88
-
89
- # internal
90
-
91
- def _is_resolved(self) -> bool:
92
- return self.dependencies is not None
93
124
 
94
125
  # implement AbstractInstanceProvider
95
126
 
96
- def resolve(self, context: Providers.Context):
127
+ def get_host(self):
128
+ return self.host
129
+
130
+ def check_factories(self):
97
131
  pass
98
132
 
99
133
  def get_module(self) -> str:
@@ -108,22 +142,11 @@ class InstanceProvider(AbstractInstanceProvider):
108
142
  def get_scope(self) -> str:
109
143
  return self.scope
110
144
 
111
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
112
- return self.dependencies
113
-
114
145
  # public
115
146
 
116
147
  def module(self) -> str:
117
148
  return self.host.__module__
118
149
 
119
- def add_dependency(self, provider: AbstractInstanceProvider):
120
- if any(issubclass(provider.get_type(), dependency.get_type()) for dependency in self.dependencies):
121
- return False
122
-
123
- self.dependencies.append(provider)
124
-
125
- return True
126
-
127
150
  @abstractmethod
128
151
  def create(self, environment: Environment, *args):
129
152
  pass
@@ -181,14 +204,11 @@ class AmbiguousProvider(AbstractInstanceProvider):
181
204
  def get_scope(self) -> str:
182
205
  return "singleton"
183
206
 
184
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
185
- return []
186
-
187
- def resolve(self, context: Providers.Context):
188
- pass
189
-
190
207
  def create(self, environment: Environment, *args):
191
- raise InjectorException(f"multiple candidates for type {self.type}")
208
+ raise DIException(f"multiple candidates for type {self.type}")
209
+
210
+ def report(self) -> str:
211
+ return "ambiguous: " + ",".join([provider.report() for provider in self.providers])
192
212
 
193
213
  def __str__(self):
194
214
  return f"AmbiguousProvider({self.type})"
@@ -204,7 +224,7 @@ class Scopes:
204
224
  def get(cls, scope: str, environment: Environment):
205
225
  scope_type = Scopes.scopes.get(scope, None)
206
226
  if scope_type is None:
207
- raise InjectorException(f"unknown scope type {scope}")
227
+ raise DIRegistrationException(f"unknown scope type {scope}")
208
228
 
209
229
  return environment.get(scope_type)
210
230
 
@@ -234,8 +254,8 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
234
254
  __slots__ = [
235
255
  "environment",
236
256
  "scope_instance",
237
- "provider",
238
257
  "dependencies",
258
+ "provider"
239
259
  ]
240
260
 
241
261
  # constructor
@@ -245,14 +265,30 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
245
265
 
246
266
  self.environment = environment
247
267
  self.provider = provider
248
- self.dependencies : list[AbstractInstanceProvider] = []
249
-
268
+ self.dependencies = []
250
269
  self.scope_instance = Scopes.get(provider.get_scope(), environment)
251
270
 
252
271
  # implement
253
272
 
254
- def resolve(self, context: Providers.Context):
255
- pass # noop
273
+ def resolve(self, context: Providers.ResolveContext):
274
+ context.add(self)
275
+
276
+ if not context.is_resolved(self):
277
+ context.provider_dependencies[self] = [] #?
278
+
279
+ type_and_params = self.provider.get_dependencies()
280
+ params = type_and_params[1]
281
+ for type in type_and_params[0]:
282
+ if params > 0:
283
+ params -= 1
284
+ self.dependencies.append(context.get_provider(type))
285
+
286
+ provider = context.add_provider_dependency(self, type)
287
+ if provider is not None:
288
+ provider.resolve(context)
289
+
290
+ else:
291
+ context.add(*context.get_provider_dependencies(self))
256
292
 
257
293
  def get_module(self) -> str:
258
294
  return self.provider.get_module()
@@ -266,18 +302,10 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
266
302
  def get_scope(self) -> str:
267
303
  return self.provider.get_scope()
268
304
 
269
- # custom logic
270
-
271
- def tweak_dependencies(self, providers: dict[Type, AbstractInstanceProvider]):
272
- for dependency in self.provider.get_dependencies():
273
- instance_provider = providers.get(dependency.get_type(), None)
274
- if instance_provider is None:
275
- raise InjectorException(f"missing import for {dependency.get_type()} ")
305
+ def report(self) -> str:
306
+ return self.provider.report()
276
307
 
277
- self.dependencies.append(instance_provider)
278
-
279
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
280
- return self.provider.get_dependencies()
308
+ # own logic
281
309
 
282
310
  def create(self, environment: Environment, *args):
283
311
  return self.scope_instance.get(self.provider, self.environment, lambda: [provider.create(environment) for provider in self.dependencies]) # already scope property!
@@ -303,41 +331,39 @@ class ClassInstanceProvider(InstanceProvider):
303
331
 
304
332
  # implement
305
333
 
306
- def resolve(self, context: Providers.Context):
307
- context.add(self)
334
+ def check_factories(self):
335
+ register_factories(self.host)
308
336
 
309
- if not self._is_resolved():
310
- self.dependencies = []
337
+ def get_dependencies(self) -> (list[Type],int):
338
+ types : list[Type] = []
311
339
 
312
- # check constructor
340
+ # check constructor
313
341
 
314
- init = TypeDescriptor.for_type(self.type).get_method("__init__")
315
- if init is None:
316
- raise InjectorException(f"{self.type.__name__} does not implement __init__")
342
+ init = TypeDescriptor.for_type(self.type).get_method("__init__")
343
+ if init is None:
344
+ raise DIRegistrationException(f"{self.type.__name__} does not implement __init__")
317
345
 
318
- for param in init.param_types:
319
- provider = Providers.get_provider(param)
320
- self.params += 1
321
- if self.add_dependency(provider): # a dependency can occur multiple times, e.g in __init__ and in an injected method
322
- provider.resolve(context)
346
+ self.params = len(init.param_types)
347
+ for param in init.param_types:
348
+ types.append(param)
323
349
 
324
- # check @inject
350
+ # check @inject
325
351
 
326
- for method in TypeDescriptor.for_type(self.type).get_methods():
327
- if method.has_decorator(inject):
328
- for param in method.param_types:
329
- provider = Providers.get_provider(param)
352
+ for method in TypeDescriptor.for_type(self.type).get_methods():
353
+ if method.has_decorator(inject):
354
+ for param in method.param_types:
355
+ types.append(param)
330
356
 
331
- if self.add_dependency(provider):
332
- provider.resolve(context)
333
- else: # check if the dependencies create a cycle
334
- context.add(*self.dependencies)
357
+ return (types, self.params)
335
358
 
336
359
  def create(self, environment: Environment, *args):
337
360
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
338
361
 
339
362
  return environment.created(self.type(*args[:self.params]))
340
363
 
364
+ def report(self) -> str:
365
+ return f"{self.host.__name__}.__init__"
366
+
341
367
  # object
342
368
 
343
369
  def __str__(self):
@@ -361,25 +387,19 @@ class FunctionInstanceProvider(InstanceProvider):
361
387
 
362
388
  # implement
363
389
 
364
- def resolve(self, context: Providers.Context):
365
- context.add(self)
366
-
367
- if not self._is_resolved():
368
- self.dependencies = []
369
-
370
- provider = Providers.get_provider(self.host)
371
- if self.add_dependency(provider):
372
- provider.resolve(context)
373
- else: # check if the dependencies crate a cycle
374
- context.add(*self.dependencies)
390
+ def get_dependencies(self) -> (list[Type],int):
391
+ return [self.host], 1
375
392
 
376
393
  def create(self, environment: Environment, *args):
377
394
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
378
395
 
379
- instance = self.method(*args)
396
+ instance = self.method(*args) # args[0]=self
380
397
 
381
398
  return environment.created(instance)
382
399
 
400
+ def report(self) -> str:
401
+ return f"{self.host.__name__}.{self.method.__name__}"
402
+
383
403
  def __str__(self):
384
404
  return f"FunctionInstanceProvider({self.host.__name__}.{self.method.__name__} -> {self.type.__name__})"
385
405
 
@@ -403,33 +423,24 @@ class FactoryInstanceProvider(InstanceProvider):
403
423
 
404
424
  # implement
405
425
 
406
- def resolve(self, context: Providers.Context):
407
- context.add(self)
408
-
409
- if not self._is_resolved():
410
- self.dependencies = []
411
-
412
- provider = Providers.get_provider(self.host)
413
- if self.add_dependency(provider):
414
- provider.resolve(context)
415
-
416
- else: # check if the dependencies crate a cycle
417
- context.add(*self.dependencies)
418
-
419
- return self
426
+ def get_dependencies(self) -> (list[Type],int):
427
+ return [self.host],1
420
428
 
421
429
  def create(self, environment: Environment, *args):
422
430
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
423
431
 
424
432
  return environment.created(args[0].create())
425
433
 
434
+ def report(self) -> str:
435
+ return f"{self.host.__name__}.create"
436
+
426
437
  def __str__(self):
427
438
  return f"FactoryInstanceProvider({self.host.__name__} -> {self.type.__name__})"
428
439
 
429
440
 
430
441
  class Lifecycle(Enum):
431
442
  """
432
- This enum defines the lifecycle events that can be processed by lifecycle processors.
443
+ This enum defines the lifecycle phases that can be processed by lifecycle processors.
433
444
  """
434
445
 
435
446
  __slots__ = []
@@ -441,7 +452,7 @@ class Lifecycle(Enum):
441
452
 
442
453
  class LifecycleProcessor(ABC):
443
454
  """
444
- A LifecycleProcessor is used to perform any side effects on managed objects during their lifecycle.
455
+ A LifecycleProcessor is used to perform any side effects on managed objects during different lifecycle phases.
445
456
  """
446
457
  __slots__ = [
447
458
  "order"
@@ -481,16 +492,57 @@ class Providers:
481
492
  """
482
493
  # local class
483
494
 
484
- class Context:
485
- __slots__ = ["dependencies"]
495
+ class ResolveContext:
496
+ __slots__ = [
497
+ "dependencies",
498
+ "providers",
499
+ "provider_dependencies"
500
+ ]
501
+
502
+ # constructor
503
+
504
+ def __init__(self, providers: Dict[Type, EnvironmentInstanceProvider]):
505
+ self.dependencies : list[EnvironmentInstanceProvider] = []
506
+ self.providers = providers
507
+ self.provider_dependencies : dict[EnvironmentInstanceProvider, list[EnvironmentInstanceProvider]] = {}
508
+
509
+ # public
510
+
511
+ def is_resolved(self, provider: EnvironmentInstanceProvider) -> bool:
512
+ return self.provider_dependencies.get(provider, None) is not None
513
+
514
+ def get_provider_dependencies(self, provider: EnvironmentInstanceProvider) -> list[EnvironmentInstanceProvider]:
515
+ return self.provider_dependencies[provider]
516
+
517
+ def add_provider_dependency(self, provider: EnvironmentInstanceProvider, type: Type) -> Optional[EnvironmentInstanceProvider]:
518
+ provider_dependencies = self.provider_dependencies.get(provider, None)
519
+ if provider_dependencies is None:
520
+ provider_dependencies = []
521
+ self.provider_dependencies[provider] = provider_dependencies
522
+
523
+ provider = self.get_provider(type)
486
524
 
487
- def __init__(self):
488
- self.dependencies : list[AbstractInstanceProvider] = []
525
+ if any(issubclass(provider.get_type(), dependency.get_type()) for dependency in provider_dependencies):
526
+ return None
489
527
 
490
- def add(self, *providers: AbstractInstanceProvider):
528
+ provider_dependencies.append(provider)
529
+
530
+ return provider
531
+
532
+ def next(self):
533
+ self.dependencies.clear()
534
+
535
+ def get_provider(self, type: Type) -> EnvironmentInstanceProvider:
536
+ provider = self.providers.get(type, None)
537
+ if provider is None:
538
+ raise DIRegistrationException(f"Provider for {type} is not defined")
539
+
540
+ return provider
541
+
542
+ def add(self, *providers: EnvironmentInstanceProvider):
491
543
  for provider in providers:
492
544
  if next((p for p in self.dependencies if p.get_type() is provider.get_type()), None) is not None:
493
- raise InjectorException(self.cycle_report(provider))
545
+ raise DIRegistrationException(self.cycle_report(provider))
494
546
 
495
547
  self.dependencies.append(provider)
496
548
 
@@ -504,19 +556,17 @@ class Providers:
504
556
 
505
557
  first = False
506
558
 
507
- cycle += f"{p.get_type().__name__}"
559
+ cycle += f"{p.report()}"
508
560
 
509
- cycle += f" <> {provider.get_type().__name__}"
561
+ cycle += f" <> {provider.report()}"
510
562
 
511
563
  return cycle
512
564
 
513
565
 
514
566
  # class properties
515
567
 
516
- check: list[AbstractInstanceProvider] = []
517
-
518
- providers : Dict[Type,AbstractInstanceProvider] = {}
519
- cache: Dict[Type, AbstractInstanceProvider] = {}
568
+ check : list[AbstractInstanceProvider] = []
569
+ providers : Dict[Type,list[AbstractInstanceProvider]] = {}
520
570
 
521
571
  resolved = False
522
572
 
@@ -524,7 +574,58 @@ class Providers:
524
574
  def register(cls, provider: AbstractInstanceProvider):
525
575
  Environment.logger.debug("register provider %s(%s)", provider.get_type().__qualname__, provider.get_type().__name__)
526
576
 
527
- # local functions
577
+ Providers.check.append(provider)
578
+ candidates = Providers.providers.get(provider.get_type(), None)
579
+ if candidates is None:
580
+ Providers.providers[provider.get_type()] = [provider]
581
+ else:
582
+ candidates.append(provider)
583
+
584
+ # add factories lazily
585
+
586
+ @classmethod
587
+ def check_factories(cls):
588
+ for check in Providers.check:
589
+ check.check_factories()
590
+
591
+ Providers.check.clear()
592
+
593
+ @classmethod
594
+ def filter(cls, environment: Environment) -> Dict[Type,AbstractInstanceProvider]:
595
+ cache: Dict[Type,AbstractInstanceProvider] = {}
596
+
597
+ context: ConditionContext = {
598
+ "requires_feature": lambda feature : environment.has_feature(feature),
599
+ "requires_class": lambda clazz : cache.get(clazz, None) is not None # ? only works if the class is in the cache already?
600
+ }
601
+
602
+ Providers.check_factories() # check for additional factories
603
+
604
+ # local methods
605
+
606
+ def filter_type(clazz: Type) -> Optional[AbstractInstanceProvider]:
607
+ result = None
608
+ for provider in Providers.providers[clazz]:
609
+ if provider_applies(provider):
610
+ if result is not None:
611
+ raise ProviderCollisionException(f"type {clazz.__name__} already registered", result, provider)
612
+
613
+ else:
614
+ result = provider
615
+
616
+ return result
617
+
618
+ def provider_applies(provider: AbstractInstanceProvider) -> bool:
619
+ descriptor = TypeDescriptor.for_type(provider.get_host())
620
+ if descriptor.has_decorator(conditional):
621
+ conditions: list[Condition] = [*descriptor.get_decorator(conditional).args]
622
+ for condition in conditions:
623
+ if not condition.apply(context):
624
+ return False
625
+
626
+ return True
627
+
628
+ return True
528
629
 
529
630
  def is_injectable(type: Type) -> bool:
530
631
  if type is object:
@@ -533,27 +634,21 @@ class Providers:
533
634
  if inspect.isabstract(type):
534
635
  return False
535
636
 
536
- #for decorator in Decorators.get(type):
537
- # if decorator.decorator is injectable:
538
- # return True
539
-
540
- # darn
541
-
542
637
  return True
543
638
 
544
639
  def cache_provider_for_type(provider: AbstractInstanceProvider, type: Type):
545
- existing_provider = Providers.cache.get(type)
640
+ existing_provider = cache.get(type)
546
641
  if existing_provider is None:
547
- Providers.cache[type] = provider
642
+ cache[type] = provider
548
643
 
549
644
  else:
550
645
  if type is provider.get_type():
551
- raise InjectorException(f"{type} already registered")
646
+ raise ProviderCollisionException(f"type {type.__name__} already registered", existing_provider, provider)
552
647
 
553
648
  if isinstance(existing_provider, AmbiguousProvider):
554
649
  cast(AmbiguousProvider, existing_provider).add_provider(provider)
555
650
  else:
556
- Providers.cache[type] = AmbiguousProvider(type, existing_provider, provider)
651
+ cache[type] = AmbiguousProvider(type, existing_provider, provider)
557
652
 
558
653
  # recursion
559
654
 
@@ -561,35 +656,35 @@ class Providers:
561
656
  if is_injectable(super_class):
562
657
  cache_provider_for_type(provider, super_class)
563
658
 
564
- # go
659
+ # filter conditional providers and fill base classes as well
565
660
 
566
- Providers.check.append(provider)
661
+ for provider_type, providers in Providers.providers.items():
662
+ matching_provider = filter_type(provider_type)
663
+ if matching_provider is not None:
664
+ cache_provider_for_type(matching_provider, provider_type)
567
665
 
568
- Providers.providers[provider.get_type()] = provider
666
+ # replace by EnvironmentInstanceProvider
569
667
 
570
- # cache providers
668
+ mapped = {}
669
+ result = {}
670
+ for provider_type, provider in cache.items():
671
+ environment_provider = mapped.get(provider, None)
672
+ if environment_provider is None:
673
+ environment_provider = EnvironmentInstanceProvider(environment, provider)
674
+ mapped[provider] = environment_provider
571
675
 
572
- cache_provider_for_type(provider, provider.get_type())
676
+ result[provider_type] = environment_provider
573
677
 
574
- @classmethod
575
- def resolve(cls):
576
- for provider in Providers.check:
577
- provider.resolve(Providers.Context())
678
+ # and resolve
578
679
 
579
- Providers.check.clear()
680
+ provider_context = Providers.ResolveContext(result)
681
+ for provider in mapped.values():
682
+ provider.resolve(provider_context)
683
+ provider_context.next() # clear dependencies
580
684
 
581
- @classmethod
582
- def report(cls):
583
- for provider in Providers.cache.values():
584
- print(f"provider {provider.get_type().__qualname__}")
585
-
586
- @classmethod
587
- def get_provider(cls, type: Type) -> AbstractInstanceProvider:
588
- provider = Providers.cache.get(type, None)
589
- if provider is None:
590
- raise InjectorException(f"{type.__name__} not registered as injectable")
685
+ # done
591
686
 
592
- return provider
687
+ return result
593
688
 
594
689
  def register_factories(cls: Type):
595
690
  descriptor = TypeDescriptor.for_type(cls)
@@ -597,7 +692,11 @@ def register_factories(cls: Type):
597
692
  for method in descriptor.get_methods():
598
693
  if method.has_decorator(create):
599
694
  create_decorator = method.get_decorator(create)
600
- Providers.register(FunctionInstanceProvider(cls, method.method, method.return_type, create_decorator.args[0],
695
+ return_type = method.return_type
696
+ if return_type is None:
697
+ raise DIRegistrationException(f"{cls.__name__}.{method.method.__name__} expected to have a return type")
698
+
699
+ Providers.register(FunctionInstanceProvider(cls, method.method, return_type, create_decorator.args[0],
601
700
  create_decorator.args[1]))
602
701
  def order(prio = 0):
603
702
  def decorator(cls):
@@ -686,8 +785,6 @@ def environment(imports: Optional[list[Type]] = None):
686
785
  Decorators.add(cls, environment, imports)
687
786
  Decorators.add(cls, injectable) # do we need that?
688
787
 
689
- register_factories(cls)
690
-
691
788
  return cls
692
789
 
693
790
  return decorator
@@ -712,6 +809,49 @@ def inject_environment():
712
809
 
713
810
  return decorator
714
811
 
812
+ # conditional stuff
813
+
814
+ class ConditionContext(TypedDict):
815
+ requires_feature: Callable[[str], bool]
816
+ requires_class: Callable[[Type], bool]
817
+
818
+ class Condition(ABC):
819
+ @abstractmethod
820
+ def apply(self, context: ConditionContext) -> bool:
821
+ pass
822
+
823
+ class FeatureCondition(Condition):
824
+ def __init__(self, feature: str):
825
+ super().__init__()
826
+
827
+ self.feature = feature
828
+
829
+ def apply(self, context: ConditionContext) -> bool:
830
+ return context["requires_feature"](self.feature)
831
+
832
+ class ClassCondition(Condition):
833
+ def __init__(self, clazz: Type):
834
+ super().__init__()
835
+
836
+ self.clazz = clazz
837
+
838
+ def apply(self, context: ConditionContext) -> bool:
839
+ return context["requires_class"](self.clazz)
840
+
841
+ def requires_feature(feature: str):
842
+ return FeatureCondition(feature)
843
+
844
+ def requires_class(clazz: Type):
845
+ return ClassCondition(clazz)
846
+
847
+ def conditional(*conditions: Condition):
848
+ def decorator(cls):
849
+ Decorators.add(cls, conditional, *conditions)
850
+
851
+ return cls
852
+
853
+ return decorator
854
+
715
855
  class Environment:
716
856
  """
717
857
  Central class that manages the lifecycle of instances and their dependencies.
@@ -728,12 +868,13 @@ class Environment:
728
868
  "providers",
729
869
  "lifecycle_processors",
730
870
  "parent",
871
+ "features",
731
872
  "instances"
732
873
  ]
733
874
 
734
875
  # constructor
735
876
 
736
- def __init__(self, env: Type, parent : Optional[Environment] = None):
877
+ def __init__(self, env: Type, features: list[str] = [], parent : Optional[Environment] = None):
737
878
  """
738
879
  Creates a new Environment instance.
739
880
 
@@ -741,27 +882,34 @@ class Environment:
741
882
  env (Type): The environment class that controls the scanning of managed objects.
742
883
  parent (Optional[Environment]): Optional parent environment, whose objects are inherited.
743
884
  """
885
+
886
+ Environment.logger.debug("create environment for class %s", env.__qualname__)
887
+
744
888
  # initialize
745
889
 
746
890
  self.type = env
747
891
  self.parent = parent
748
- if self.parent is None and env is not BootEnvironment:
749
- self.parent = BootEnvironment.get_instance() # inherit environment including its manged instances!
892
+ if self.parent is None and env is not Boot:
893
+ self.parent = Boot.get_environment() # inherit environment including its manged instances!
750
894
 
895
+ self.features = features
751
896
  self.providers: Dict[Type, AbstractInstanceProvider] = {}
752
897
  self.lifecycle_processors: list[LifecycleProcessor] = []
753
898
 
754
899
  if self.parent is not None:
755
900
  self.providers |= self.parent.providers
756
901
  self.lifecycle_processors += self.parent.lifecycle_processors
902
+ else: #if self.type is Boot:
903
+ self.providers[SingletonScope] = SingletonScopeInstanceProvider()
904
+ self.providers[RequestScope] = RequestScopeInstanceProvider()
757
905
 
758
906
  self.instances = []
759
907
 
760
908
  Environment.instance = self
761
909
 
762
- # resolve providers on a static basis. This is executed for all new providers ( in case of new modules multiple times) !
910
+ # filter conditional providers
763
911
 
764
- Providers.resolve()
912
+ overall_providers = Providers.filter(self)
765
913
 
766
914
  loaded = set()
767
915
 
@@ -770,12 +918,6 @@ class Environment:
770
918
 
771
919
  self.providers[type] = provider
772
920
 
773
- # bootstrapping hack, they will be overwritten by the "real" providers
774
-
775
- if env is BootEnvironment:
776
- add_provider(SingletonScope, SingletonScopeInstanceProvider())
777
- add_provider(RequestScope, RequestScopeInstanceProvider())
778
-
779
921
  def load_environment(env: Type):
780
922
  if env not in loaded:
781
923
  Environment.logger.debug("load environment %s", env.__qualname__)
@@ -786,7 +928,7 @@ class Environment:
786
928
 
787
929
  decorator = TypeDescriptor.for_type(env).get_decorator(environment)
788
930
  if decorator is None:
789
- raise InjectorException(f"{env.__name__} is not an environment class")
931
+ raise DIRegistrationException(f"{env.__name__} is not an environment class")
790
932
 
791
933
  scan = env.__module__
792
934
  if "." in scan:
@@ -797,39 +939,18 @@ class Environment:
797
939
  for import_environment in decorator.args[0] or []:
798
940
  load_environment(import_environment)
799
941
 
800
- # load providers
801
-
802
- local_providers = {type: provider for type, provider in Providers.cache.items() if provider.get_module().startswith(scan)}
803
-
804
- # register providers
805
-
806
- # make sure, that for every type only a single EnvironmentInstanceProvider is created!
807
- # otherwise inheritance will fuck it up
808
-
809
- environment_providers : dict[AbstractInstanceProvider, EnvironmentInstanceProvider] = {}
810
-
811
- for type, provider in local_providers.items():
812
- environment_provider = environment_providers.get(provider, None)
813
- if environment_provider is None:
814
- environment_provider = EnvironmentInstanceProvider(self, provider)
815
- environment_providers[provider] = environment_provider
816
-
817
- add_provider(type, environment_provider)
942
+ # filter and load providers according to their module
818
943
 
819
- # tweak dependencies
820
-
821
- for type, provider in local_providers.items():
822
- cast(EnvironmentInstanceProvider, self.providers[type]).tweak_dependencies(self.providers)
823
-
824
- # return local providers
944
+ for type, provider in overall_providers.items():
945
+ if provider.get_module().startswith(scan):
946
+ add_provider(type, provider)
947
+ # go
825
948
 
826
- return environment_providers.values()
827
- else:
828
- return []
949
+ load_environment(env)
829
950
 
830
951
  # construct eager objects for local providers
831
952
 
832
- for provider in load_environment(env):
953
+ for provider in set(self.providers.values()):
833
954
  if provider.is_eager():
834
955
  provider.create(self)
835
956
 
@@ -840,6 +961,9 @@ class Environment:
840
961
 
841
962
  # internal
842
963
 
964
+ def has_feature(self, feature: str) -> bool:
965
+ return feature in self.features
966
+
843
967
  def execute_processors(self, lifecycle: Lifecycle, instance: T) -> T:
844
968
  for processor in self.lifecycle_processors:
845
969
  processor.process_lifecycle(lifecycle, instance, self)
@@ -889,8 +1013,9 @@ class Environment:
889
1013
 
890
1014
  builder.append("Providers \n")
891
1015
  for result_type, provider in self.providers.items():
892
- if cast(EnvironmentInstanceProvider, provider).environment is self:
893
- builder.append(f"- {result_type.__name__}: {cast(EnvironmentInstanceProvider, provider).provider}\n")
1016
+ if isinstance(provider, EnvironmentInstanceProvider):
1017
+ if cast(EnvironmentInstanceProvider, provider).environment is self:
1018
+ builder.append(f"- {result_type.__name__}: {provider.report()}\n")
894
1019
 
895
1020
  # instances
896
1021
 
@@ -921,7 +1046,7 @@ class Environment:
921
1046
 
922
1047
  def get(self, type: Type[T]) -> T:
923
1048
  """
924
- Return and possibly create a new instance of the given type.
1049
+ Create or return a cached instance for the given type.
925
1050
 
926
1051
  Arguments:
927
1052
  type (Type): The desired type
@@ -931,10 +1056,13 @@ class Environment:
931
1056
  provider = self.providers.get(type, None)
932
1057
  if provider is None:
933
1058
  Environment.logger.error("%s is not supported", type)
934
- raise InjectorException(f"{type} is not supported")
1059
+ raise DIRuntimeException(f"{type} is not supported")
935
1060
 
936
1061
  return provider.create(self)
937
1062
 
1063
+ def __str__(self):
1064
+ return f"Environment({self.type.__name__})"
1065
+
938
1066
  class LifecycleCallable:
939
1067
  __slots__ = [
940
1068
  "decorator",
@@ -981,6 +1109,7 @@ class AbstractCallableProcessor(LifecycleProcessor):
981
1109
 
982
1110
  # static data
983
1111
 
1112
+ lock = threading.RLock()
984
1113
  callables : Dict[object, LifecycleCallable] = {}
985
1114
  cache : Dict[Type, list[list[AbstractCallableProcessor.MethodCall]]] = {}
986
1115
 
@@ -1018,8 +1147,11 @@ class AbstractCallableProcessor(LifecycleProcessor):
1018
1147
  def callables_for(cls, type: Type) -> list[list[AbstractCallableProcessor.MethodCall]]:
1019
1148
  callables = AbstractCallableProcessor.cache.get(type, None)
1020
1149
  if callables is None:
1021
- callables = AbstractCallableProcessor.compute_callables(type)
1022
- AbstractCallableProcessor.cache[type] = callables
1150
+ with AbstractCallableProcessor.lock:
1151
+ callables = AbstractCallableProcessor.cache.get(type, None)
1152
+ if callables is None:
1153
+ callables = AbstractCallableProcessor.compute_callables(type)
1154
+ AbstractCallableProcessor.cache[type] = callables
1023
1155
 
1024
1156
  return callables
1025
1157
 
@@ -1113,20 +1245,20 @@ class InjectLifecycleCallable(LifecycleCallable):
1113
1245
  def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
1114
1246
  return [environment.get(type) for type in method.param_types]
1115
1247
 
1116
- def scope(name: str):
1248
+ def scope(name: str, register=True):
1117
1249
  def decorator(cls):
1118
1250
  Scopes.register(cls, name)
1119
1251
 
1120
1252
  Decorators.add(cls, scope)
1121
- # Decorators.add(cls, injectable)
1122
1253
 
1123
- Providers.register(ClassInstanceProvider(cls, eager=True, scope="request"))
1254
+ if register:
1255
+ Providers.register(ClassInstanceProvider(cls, eager=True, scope="request"))
1124
1256
 
1125
1257
  return cls
1126
1258
 
1127
1259
  return decorator
1128
1260
 
1129
- @scope("request")
1261
+ @scope("request", register=False)
1130
1262
  class RequestScope(Scope):
1131
1263
  # properties
1132
1264
 
@@ -1138,7 +1270,7 @@ class RequestScope(Scope):
1138
1270
  def get(self, provider: AbstractInstanceProvider, environment: Environment, arg_provider: Callable[[],list]):
1139
1271
  return provider.create(environment, *arg_provider())
1140
1272
 
1141
- @scope("singleton")
1273
+ @scope("singleton", register=False)
1142
1274
  class SingletonScope(Scope):
1143
1275
  # properties
1144
1276
 
@@ -1164,7 +1296,7 @@ class SingletonScope(Scope):
1164
1296
  self.value = provider.create(environment, *arg_provider())
1165
1297
 
1166
1298
  return self.value
1167
-
1299
+
1168
1300
  @scope("thread")
1169
1301
  class ThreadScope(Scope):
1170
1302
  __slots__ = [
@@ -1185,17 +1317,17 @@ class ThreadScope(Scope):
1185
1317
  # internal class that is required to import technical instance providers
1186
1318
 
1187
1319
  @environment()
1188
- class BootEnvironment:
1320
+ class Boot:
1189
1321
  # class
1190
1322
 
1191
1323
  environment = None
1192
1324
 
1193
1325
  @classmethod
1194
- def get_instance(cls):
1195
- if BootEnvironment.environment is None:
1196
- BootEnvironment.environment = Environment(BootEnvironment)
1326
+ def get_environment(cls):
1327
+ if Boot.environment is None:
1328
+ Boot.environment = Environment(Boot)
1197
1329
 
1198
- return BootEnvironment.environment
1330
+ return Boot.environment
1199
1331
 
1200
1332
  # properties
1201
1333