aspyx 1.2.0__py3-none-any.whl → 1.4.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
@@ -5,13 +5,16 @@ from __future__ import annotations
5
5
 
6
6
  import inspect
7
7
  import logging
8
+ import importlib
9
+ import pkgutil
10
+ import sys
8
11
 
9
12
  from abc import abstractmethod, ABC
10
13
  from enum import Enum
11
14
  import threading
12
- from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable
15
+ from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable, TypedDict
13
16
 
14
- from aspyx.di.util import StringBuilder
17
+ from aspyx.util import StringBuilder
15
18
  from aspyx.reflection import Decorators, TypeDescriptor, DecoratorDescriptor
16
19
 
17
20
  T = TypeVar("T")
@@ -31,16 +34,31 @@ class DIException(Exception):
31
34
  """
32
35
  Exception raised for errors in the injector.
33
36
  """
37
+ def __init__(self, message: str):
38
+ super().__init__(message)
34
39
 
35
40
  class DIRegistrationException(DIException):
36
41
  """
37
42
  Exception raised during the registration of dependencies.
38
43
  """
44
+ def __init__(self, message: str):
45
+ super().__init__(message)
46
+
47
+ class ProviderCollisionException(DIRegistrationException):
48
+ def __init__(self, message: str, *providers: AbstractInstanceProvider):
49
+ super().__init__(message)
50
+
51
+ self.providers = providers
52
+
53
+ def __str__(self):
54
+ return f"[{self.args[0]} {self.providers[1].location()} collides with {self.providers[0].location()}"
39
55
 
40
56
  class DIRuntimeException(DIException):
41
57
  """
42
58
  Exception raised during the runtime.
43
59
  """
60
+ def __init__(self, message: str):
61
+ super().__init__(message)
44
62
 
45
63
  class AbstractInstanceProvider(ABC, Generic[T]):
46
64
  """
@@ -65,17 +83,23 @@ class AbstractInstanceProvider(ABC, Generic[T]):
65
83
  def get_scope(self) -> str:
66
84
  pass
67
85
 
68
- @abstractmethod
69
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
70
- pass
86
+ def get_dependencies(self) -> (list[Type],int):
87
+ return [],1
71
88
 
72
89
  @abstractmethod
73
90
  def create(self, environment: Environment, *args):
74
91
  pass
75
92
 
76
- @abstractmethod
77
- def resolve(self, context: Providers.Context):
78
- pass
93
+ def report(self) -> str:
94
+ return str(self)
95
+
96
+ def location(self) -> str:
97
+ host = self.get_host()
98
+
99
+ file = inspect.getfile(host)
100
+ line = inspect.getsourcelines(host)[1]
101
+
102
+ return f"{file}:{line}"
79
103
 
80
104
  def check_factories(self):
81
105
  pass
@@ -89,8 +113,7 @@ class InstanceProvider(AbstractInstanceProvider):
89
113
  "host",
90
114
  "type",
91
115
  "eager",
92
- "scope",
93
- "dependencies"
116
+ "scope"
94
117
  ]
95
118
 
96
119
  # constructor
@@ -100,12 +123,6 @@ class InstanceProvider(AbstractInstanceProvider):
100
123
  self.type = t
101
124
  self.eager = eager
102
125
  self.scope = scope
103
- self.dependencies : Optional[list[AbstractInstanceProvider]] = None
104
-
105
- # internal
106
-
107
- def _is_resolved(self) -> bool:
108
- return self.dependencies is not None
109
126
 
110
127
  # implement AbstractInstanceProvider
111
128
 
@@ -113,10 +130,6 @@ class InstanceProvider(AbstractInstanceProvider):
113
130
  return self.host
114
131
 
115
132
  def check_factories(self):
116
- #register_factories(self.host)
117
- pass
118
-
119
- def resolve(self, context: Providers.Context):
120
133
  pass
121
134
 
122
135
  def get_module(self) -> str:
@@ -131,22 +144,11 @@ class InstanceProvider(AbstractInstanceProvider):
131
144
  def get_scope(self) -> str:
132
145
  return self.scope
133
146
 
134
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
135
- return self.dependencies
136
-
137
147
  # public
138
148
 
139
149
  def module(self) -> str:
140
150
  return self.host.__module__
141
151
 
142
- def add_dependency(self, provider: AbstractInstanceProvider):
143
- if any(issubclass(provider.get_type(), dependency.get_type()) for dependency in self.dependencies):
144
- return False
145
-
146
- self.dependencies.append(provider)
147
-
148
- return True
149
-
150
152
  @abstractmethod
151
153
  def create(self, environment: Environment, *args):
152
154
  pass
@@ -159,6 +161,13 @@ class SingletonScopeInstanceProvider(InstanceProvider):
159
161
  def create(self, environment: Environment, *args):
160
162
  return SingletonScope()
161
163
 
164
+ class EnvironmentScopeInstanceProvider(InstanceProvider):
165
+ def __init__(self):
166
+ super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request") # TODO?
167
+
168
+ def create(self, environment: Environment, *args):
169
+ return EnvironmentScope()
170
+
162
171
  class RequestScopeInstanceProvider(InstanceProvider):
163
172
  def __init__(self):
164
173
  super().__init__(RequestScopeInstanceProvider, RequestScope, False, "singleton")
@@ -166,7 +175,6 @@ class RequestScopeInstanceProvider(InstanceProvider):
166
175
  def create(self, environment: Environment, *args):
167
176
  return RequestScope()
168
177
 
169
-
170
178
  class AmbiguousProvider(AbstractInstanceProvider):
171
179
  """
172
180
  An AmbiguousProvider covers all cases, where fetching a class would lead to an ambiguity exception.
@@ -204,15 +212,12 @@ class AmbiguousProvider(AbstractInstanceProvider):
204
212
  def get_scope(self) -> str:
205
213
  return "singleton"
206
214
 
207
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
208
- return []
209
-
210
- def resolve(self, context: Providers.Context):
211
- pass
212
-
213
215
  def create(self, environment: Environment, *args):
214
216
  raise DIException(f"multiple candidates for type {self.type}")
215
217
 
218
+ def report(self) -> str:
219
+ return "ambiguous: " + ",".join([provider.report() for provider in self.providers])
220
+
216
221
  def __str__(self):
217
222
  return f"AmbiguousProvider({self.type})"
218
223
 
@@ -257,8 +262,8 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
257
262
  __slots__ = [
258
263
  "environment",
259
264
  "scope_instance",
260
- "provider",
261
265
  "dependencies",
266
+ "provider"
262
267
  ]
263
268
 
264
269
  # constructor
@@ -268,14 +273,30 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
268
273
 
269
274
  self.environment = environment
270
275
  self.provider = provider
271
- self.dependencies : list[AbstractInstanceProvider] = []
272
-
276
+ self.dependencies = []
273
277
  self.scope_instance = Scopes.get(provider.get_scope(), environment)
274
278
 
275
279
  # implement
276
280
 
277
- def resolve(self, context: Providers.Context):
278
- pass # noop
281
+ def resolve(self, context: Providers.ResolveContext):
282
+ context.add(self)
283
+
284
+ if not context.is_resolved(self):
285
+ context.provider_dependencies[self] = [] #?
286
+
287
+ type_and_params = self.provider.get_dependencies()
288
+ params = type_and_params[1]
289
+ for type in type_and_params[0]:
290
+ if params > 0:
291
+ params -= 1
292
+ self.dependencies.append(context.get_provider(type))
293
+
294
+ provider = context.add_provider_dependency(self, type)
295
+ if provider is not None:
296
+ provider.resolve(context)
297
+
298
+ else:
299
+ context.add(*context.get_provider_dependencies(self))
279
300
 
280
301
  def get_module(self) -> str:
281
302
  return self.provider.get_module()
@@ -289,18 +310,10 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
289
310
  def get_scope(self) -> str:
290
311
  return self.provider.get_scope()
291
312
 
292
- # custom logic
293
-
294
- def tweak_dependencies(self, providers: dict[Type, AbstractInstanceProvider]):
295
- for dependency in self.provider.get_dependencies():
296
- instance_provider = providers.get(dependency.get_type(), None)
297
- if instance_provider is None:
298
- raise DIRegistrationException(f"missing import for {dependency.get_type()} ")
313
+ def report(self) -> str:
314
+ return self.provider.report()
299
315
 
300
- self.dependencies.append(instance_provider)
301
-
302
- def get_dependencies(self) -> list[AbstractInstanceProvider]:
303
- return self.provider.get_dependencies()
316
+ # own logic
304
317
 
305
318
  def create(self, environment: Environment, *args):
306
319
  return self.scope_instance.get(self.provider, self.environment, lambda: [provider.create(environment) for provider in self.dependencies]) # already scope property!
@@ -329,41 +342,36 @@ class ClassInstanceProvider(InstanceProvider):
329
342
  def check_factories(self):
330
343
  register_factories(self.host)
331
344
 
332
- def resolve(self, context: Providers.Context):
333
- context.add(self)
334
-
335
- if not self._is_resolved():
336
- self.dependencies = []
345
+ def get_dependencies(self) -> (list[Type],int):
346
+ types : list[Type] = []
337
347
 
338
- # check constructor
348
+ # check constructor
339
349
 
340
- init = TypeDescriptor.for_type(self.type).get_method("__init__")
341
- if init is None:
342
- raise DIRegistrationException(f"{self.type.__name__} does not implement __init__")
350
+ init = TypeDescriptor.for_type(self.type).get_method("__init__")
351
+ if init is None:
352
+ raise DIRegistrationException(f"{self.type.__name__} does not implement __init__")
343
353
 
344
- for param in init.param_types:
345
- provider = Providers.get_provider(param)
346
- self.params += 1
347
- if self.add_dependency(provider): # a dependency can occur multiple times, e.g in __init__ and in an injected method
348
- provider.resolve(context)
354
+ self.params = len(init.param_types)
355
+ for param in init.param_types:
356
+ types.append(param)
349
357
 
350
- # check @inject
358
+ # check @inject
351
359
 
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
- provider = Providers.get_provider(param)
360
+ for method in TypeDescriptor.for_type(self.type).get_methods():
361
+ if method.has_decorator(inject):
362
+ for param in method.param_types:
363
+ types.append(param)
356
364
 
357
- if self.add_dependency(provider):
358
- provider.resolve(context)
359
- else: # check if the dependencies create a cycle
360
- context.add(*self.dependencies)
365
+ return (types, self.params)
361
366
 
362
367
  def create(self, environment: Environment, *args):
363
368
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
364
369
 
365
370
  return environment.created(self.type(*args[:self.params]))
366
371
 
372
+ def report(self) -> str:
373
+ return f"{self.host.__name__}.__init__"
374
+
367
375
  # object
368
376
 
369
377
  def __str__(self):
@@ -387,25 +395,19 @@ class FunctionInstanceProvider(InstanceProvider):
387
395
 
388
396
  # implement
389
397
 
390
- def resolve(self, context: Providers.Context):
391
- context.add(self)
392
-
393
- if not self._is_resolved():
394
- self.dependencies = []
395
-
396
- provider = Providers.get_provider(self.host)
397
- if self.add_dependency(provider):
398
- provider.resolve(context)
399
- else: # check if the dependencies crate a cycle
400
- context.add(*self.dependencies)
398
+ def get_dependencies(self) -> (list[Type],int):
399
+ return [self.host], 1
401
400
 
402
401
  def create(self, environment: Environment, *args):
403
402
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
404
403
 
405
- instance = self.method(*args)
404
+ instance = self.method(*args) # args[0]=self
406
405
 
407
406
  return environment.created(instance)
408
407
 
408
+ def report(self) -> str:
409
+ return f"{self.host.__name__}.{self.method.__name__}"
410
+
409
411
  def __str__(self):
410
412
  return f"FunctionInstanceProvider({self.host.__name__}.{self.method.__name__} -> {self.type.__name__})"
411
413
 
@@ -429,32 +431,24 @@ class FactoryInstanceProvider(InstanceProvider):
429
431
 
430
432
  # implement
431
433
 
432
- def resolve(self, context: Providers.Context):
433
- context.add(self)
434
-
435
- if not self._is_resolved():
436
- self.dependencies = []
437
-
438
- provider = Providers.get_provider(self.host)
439
- if self.add_dependency(provider):
440
- provider.resolve(context)
441
- else: # check if the dependencies crate a cycle
442
- context.add(*self.dependencies)
443
-
444
- return self
434
+ def get_dependencies(self) -> (list[Type],int):
435
+ return [self.host],1
445
436
 
446
437
  def create(self, environment: Environment, *args):
447
438
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
448
439
 
449
440
  return environment.created(args[0].create())
450
441
 
442
+ def report(self) -> str:
443
+ return f"{self.host.__name__}.create"
444
+
451
445
  def __str__(self):
452
446
  return f"FactoryInstanceProvider({self.host.__name__} -> {self.type.__name__})"
453
447
 
454
448
 
455
449
  class Lifecycle(Enum):
456
450
  """
457
- This enum defines the lifecycle events that can be processed by lifecycle processors.
451
+ This enum defines the lifecycle phases that can be processed by lifecycle processors.
458
452
  """
459
453
 
460
454
  __slots__ = []
@@ -466,7 +460,7 @@ class Lifecycle(Enum):
466
460
 
467
461
  class LifecycleProcessor(ABC):
468
462
  """
469
- A LifecycleProcessor is used to perform any side effects on managed objects during their lifecycle.
463
+ A LifecycleProcessor is used to perform any side effects on managed objects during different lifecycle phases.
470
464
  """
471
465
  __slots__ = [
472
466
  "order"
@@ -506,13 +500,54 @@ class Providers:
506
500
  """
507
501
  # local class
508
502
 
509
- class Context:
510
- __slots__ = ["dependencies"]
503
+ class ResolveContext:
504
+ __slots__ = [
505
+ "dependencies",
506
+ "providers",
507
+ "provider_dependencies"
508
+ ]
509
+
510
+ # constructor
511
+
512
+ def __init__(self, providers: Dict[Type, EnvironmentInstanceProvider]):
513
+ self.dependencies : list[EnvironmentInstanceProvider] = []
514
+ self.providers = providers
515
+ self.provider_dependencies : dict[EnvironmentInstanceProvider, list[EnvironmentInstanceProvider]] = {}
516
+
517
+ # public
518
+
519
+ def is_resolved(self, provider: EnvironmentInstanceProvider) -> bool:
520
+ return self.provider_dependencies.get(provider, None) is not None
521
+
522
+ def get_provider_dependencies(self, provider: EnvironmentInstanceProvider) -> list[EnvironmentInstanceProvider]:
523
+ return self.provider_dependencies[provider]
524
+
525
+ def add_provider_dependency(self, provider: EnvironmentInstanceProvider, type: Type) -> Optional[EnvironmentInstanceProvider]:
526
+ provider_dependencies = self.provider_dependencies.get(provider, None)
527
+ if provider_dependencies is None:
528
+ provider_dependencies = []
529
+ self.provider_dependencies[provider] = provider_dependencies
511
530
 
512
- def __init__(self):
513
- self.dependencies : list[AbstractInstanceProvider] = []
531
+ provider = self.get_provider(type)
514
532
 
515
- def add(self, *providers: AbstractInstanceProvider):
533
+ if any(issubclass(provider.get_type(), dependency.get_type()) for dependency in provider_dependencies):
534
+ return None
535
+
536
+ provider_dependencies.append(provider)
537
+
538
+ return provider
539
+
540
+ def next(self):
541
+ self.dependencies.clear()
542
+
543
+ def get_provider(self, type: Type) -> EnvironmentInstanceProvider:
544
+ provider = self.providers.get(type, None)
545
+ if provider is None:
546
+ raise DIRegistrationException(f"Provider for {type} is not defined")
547
+
548
+ return provider
549
+
550
+ def add(self, *providers: EnvironmentInstanceProvider):
516
551
  for provider in providers:
517
552
  if next((p for p in self.dependencies if p.get_type() is provider.get_type()), None) is not None:
518
553
  raise DIRegistrationException(self.cycle_report(provider))
@@ -529,19 +564,17 @@ class Providers:
529
564
 
530
565
  first = False
531
566
 
532
- cycle += f"{p.get_type().__name__}"
567
+ cycle += f"{p.report()}"
533
568
 
534
- cycle += f" <> {provider.get_type().__name__}"
569
+ cycle += f" <> {provider.report()}"
535
570
 
536
571
  return cycle
537
572
 
538
573
 
539
574
  # class properties
540
575
 
541
- check: list[AbstractInstanceProvider] = []
542
-
543
- providers : Dict[Type,AbstractInstanceProvider] = {}
544
- cache: Dict[Type, AbstractInstanceProvider] = {}
576
+ check : list[AbstractInstanceProvider] = []
577
+ providers : Dict[Type,list[AbstractInstanceProvider]] = {}
545
578
 
546
579
  resolved = False
547
580
 
@@ -549,7 +582,64 @@ class Providers:
549
582
  def register(cls, provider: AbstractInstanceProvider):
550
583
  Environment.logger.debug("register provider %s(%s)", provider.get_type().__qualname__, provider.get_type().__name__)
551
584
 
552
- # local functions
585
+ Providers.check.append(provider)
586
+ candidates = Providers.providers.get(provider.get_type(), None)
587
+ if candidates is None:
588
+ Providers.providers[provider.get_type()] = [provider]
589
+ else:
590
+ candidates.append(provider)
591
+
592
+ # add factories lazily
593
+
594
+ @classmethod
595
+ def check_factories(cls):
596
+ for check in Providers.check:
597
+ check.check_factories()
598
+
599
+ Providers.check.clear()
600
+
601
+ @classmethod
602
+ def filter(cls, environment: Environment, provider_filter: Callable) -> Dict[Type,AbstractInstanceProvider]:
603
+ cache: Dict[Type,AbstractInstanceProvider] = {}
604
+
605
+ context: ConditionContext = {
606
+ "requires_feature": lambda feature : environment.has_feature(feature),
607
+ "requires_class": lambda clazz : cache.get(clazz, None) is not None # ? only works if the class is in the cache already?
608
+ }
609
+
610
+ Providers.check_factories() # check for additional factories
611
+
612
+ # local methods
613
+
614
+ def filter_type(clazz: Type) -> Optional[AbstractInstanceProvider]:
615
+ result = None
616
+ for provider in Providers.providers[clazz]:
617
+ if provider_applies(provider):
618
+ if result is not None:
619
+ raise ProviderCollisionException(f"type {clazz.__name__} already registered", result, provider)
620
+
621
+ result = provider
622
+
623
+ return result
624
+
625
+ def provider_applies(provider: AbstractInstanceProvider) -> bool:
626
+ # is it in the right module?
627
+
628
+ if not provider_filter(provider):
629
+ return False
630
+
631
+ # check conditionals
632
+
633
+ descriptor = TypeDescriptor.for_type(provider.get_host())
634
+ if descriptor.has_decorator(conditional):
635
+ conditions: list[Condition] = [*descriptor.get_decorator(conditional).args]
636
+ for condition in conditions:
637
+ if not condition.apply(context):
638
+ return False
639
+
640
+ return True
641
+
642
+ return True
553
643
 
554
644
  def is_injectable(type: Type) -> bool:
555
645
  if type is object:
@@ -558,34 +648,21 @@ class Providers:
558
648
  if inspect.isabstract(type):
559
649
  return False
560
650
 
561
- #for decorator in Decorators.get(type):
562
- # if decorator.decorator is injectable:
563
- # return True
564
-
565
- # darn
566
-
567
651
  return True
568
652
 
569
653
  def cache_provider_for_type(provider: AbstractInstanceProvider, type: Type):
570
- def location(provider: AbstractInstanceProvider):
571
- host = provider.get_host()
572
- file = inspect.getfile(host)
573
- line = inspect.getsourcelines(host)[1]
574
-
575
- return f"{file}:{line}"
576
-
577
- existing_provider = Providers.cache.get(type)
654
+ existing_provider = cache.get(type)
578
655
  if existing_provider is None:
579
- Providers.cache[type] = provider
656
+ cache[type] = provider
580
657
 
581
658
  else:
582
659
  if type is provider.get_type():
583
- raise DIRegistrationException(f"{type} already registered in {location(existing_provider)}, override in {location(provider)}")
660
+ raise ProviderCollisionException(f"type {type.__name__} already registered", existing_provider, provider)
584
661
 
585
662
  if isinstance(existing_provider, AmbiguousProvider):
586
663
  cast(AmbiguousProvider, existing_provider).add_provider(provider)
587
664
  else:
588
- Providers.cache[type] = AmbiguousProvider(type, existing_provider, provider)
665
+ cache[type] = AmbiguousProvider(type, existing_provider, provider)
589
666
 
590
667
  # recursion
591
668
 
@@ -593,39 +670,39 @@ class Providers:
593
670
  if is_injectable(super_class):
594
671
  cache_provider_for_type(provider, super_class)
595
672
 
596
- # go
597
-
598
- Providers.check.append(provider)
673
+ # filter conditional providers and fill base classes as well
599
674
 
600
- Providers.providers[provider.get_type()] = provider
675
+ for provider_type, _ in Providers.providers.items():
676
+ matching_provider = filter_type(provider_type)
677
+ if matching_provider is not None:
678
+ cache_provider_for_type(matching_provider, provider_type)
601
679
 
602
- # cache providers
680
+ # replace by EnvironmentInstanceProvider
603
681
 
604
- cache_provider_for_type(provider, provider.get_type())
682
+ mapped = {}
683
+ result = {}
684
+ for provider_type, provider in cache.items():
685
+ environment_provider = mapped.get(provider, None)
686
+ if environment_provider is None:
687
+ environment_provider = EnvironmentInstanceProvider(environment, provider)
688
+ mapped[provider] = environment_provider
605
689
 
606
- @classmethod
607
- def resolve(cls):
690
+ result[provider_type] = environment_provider
608
691
 
609
- for check in Providers.check:
610
- check.check_factories()
692
+ # and resolve
611
693
 
612
- # in the loop additional providers may be added
613
- while len(Providers.check) > 0:
614
- check = Providers.check.pop(0)
615
- check.resolve(Providers.Context())
694
+ providers = result
695
+ if environment.parent is not None:
696
+ providers = providers | environment.parent.providers # add parent providers
616
697
 
617
- @classmethod
618
- def report(cls):
619
- for provider in Providers.cache.values():
620
- print(f"provider {provider.get_type().__qualname__}")
698
+ provider_context = Providers.ResolveContext(providers)
699
+ for provider in mapped.values():
700
+ provider.resolve(provider_context)
701
+ provider_context.next() # clear dependencies
621
702
 
622
- @classmethod
623
- def get_provider(cls, type: Type) -> AbstractInstanceProvider:
624
- provider = Providers.cache.get(type, None)
625
- if provider is None:
626
- raise DIException(f"{type.__name__} not registered as injectable")
703
+ # done
627
704
 
628
- return provider
705
+ return result
629
706
 
630
707
  def register_factories(cls: Type):
631
708
  descriptor = TypeDescriptor.for_type(cls)
@@ -750,6 +827,49 @@ def inject_environment():
750
827
 
751
828
  return decorator
752
829
 
830
+ # conditional stuff
831
+
832
+ class ConditionContext(TypedDict):
833
+ requires_feature: Callable[[str], bool]
834
+ requires_class: Callable[[Type], bool]
835
+
836
+ class Condition(ABC):
837
+ @abstractmethod
838
+ def apply(self, context: ConditionContext) -> bool:
839
+ pass
840
+
841
+ class FeatureCondition(Condition):
842
+ def __init__(self, feature: str):
843
+ super().__init__()
844
+
845
+ self.feature = feature
846
+
847
+ def apply(self, context: ConditionContext) -> bool:
848
+ return context["requires_feature"](self.feature)
849
+
850
+ class ClassCondition(Condition):
851
+ def __init__(self, clazz: Type):
852
+ super().__init__()
853
+
854
+ self.clazz = clazz
855
+
856
+ def apply(self, context: ConditionContext) -> bool:
857
+ return context["requires_class"](self.clazz)
858
+
859
+ def requires_feature(feature: str):
860
+ return FeatureCondition(feature)
861
+
862
+ def requires_class(clazz: Type):
863
+ return ClassCondition(clazz)
864
+
865
+ def conditional(*conditions: Condition):
866
+ def decorator(cls):
867
+ Decorators.add(cls, conditional, *conditions)
868
+
869
+ return cls
870
+
871
+ return decorator
872
+
753
873
  class Environment:
754
874
  """
755
875
  Central class that manages the lifecycle of instances and their dependencies.
@@ -766,12 +886,13 @@ class Environment:
766
886
  "providers",
767
887
  "lifecycle_processors",
768
888
  "parent",
889
+ "features",
769
890
  "instances"
770
891
  ]
771
892
 
772
893
  # constructor
773
894
 
774
- def __init__(self, env: Type, parent : Optional[Environment] = None):
895
+ def __init__(self, env: Type, features: list[str] = [], parent : Optional[Environment] = None):
775
896
  """
776
897
  Creates a new Environment instance.
777
898
 
@@ -779,6 +900,9 @@ class Environment:
779
900
  env (Type): The environment class that controls the scanning of managed objects.
780
901
  parent (Optional[Environment]): Optional parent environment, whose objects are inherited.
781
902
  """
903
+
904
+ Environment.logger.debug("create environment for class %s", env.__qualname__)
905
+
782
906
  # initialize
783
907
 
784
908
  self.type = env
@@ -786,20 +910,37 @@ class Environment:
786
910
  if self.parent is None and env is not Boot:
787
911
  self.parent = Boot.get_environment() # inherit environment including its manged instances!
788
912
 
913
+ self.features = features
789
914
  self.providers: Dict[Type, AbstractInstanceProvider] = {}
915
+ self.instances = []
790
916
  self.lifecycle_processors: list[LifecycleProcessor] = []
791
917
 
792
918
  if self.parent is not None:
793
- self.providers |= self.parent.providers
794
- self.lifecycle_processors += self.parent.lifecycle_processors
919
+ # inherit providers from parent
795
920
 
796
- self.instances = []
921
+ for provider_type, inherited_provider in self.parent.providers.items():
922
+ if inherited_provider.get_scope() == "environment":
923
+ # replace with own environment instance provider
924
+ self.providers[provider_type] = EnvironmentInstanceProvider(self, cast(EnvironmentInstanceProvider, inherited_provider).provider)
925
+ else:
926
+ self.providers[provider_type] = inherited_provider
797
927
 
798
- Environment.instance = self
928
+ # inherit processors as is unless they have an environment scope
799
929
 
800
- # resolve providers on a static basis. This is executed for all new providers ( in case of new modules multiple times) !
930
+ for processor in self.parent.lifecycle_processors:
931
+ if self.providers[type(processor)].get_scope() != "environment":
932
+ self.lifecycle_processors.append(processor)
933
+ else:
934
+ # create and remember
935
+ self.lifecycle_processors.append(self.get(type(processor)))
936
+ else:
937
+ self.providers[SingletonScope] = SingletonScopeInstanceProvider()
938
+ self.providers[RequestScope] = RequestScopeInstanceProvider()
939
+ self.providers[EnvironmentScope] = EnvironmentScopeInstanceProvider()
940
+
941
+ Environment.instance = self
801
942
 
802
- Providers.resolve()
943
+ prefix_list : list[str] = []
803
944
 
804
945
  loaded = set()
805
946
 
@@ -808,11 +949,35 @@ class Environment:
808
949
 
809
950
  self.providers[type] = provider
810
951
 
811
- # bootstrapping hack, they will be overwritten by the "real" providers
952
+ def get_type_package(type: Type):
953
+ module_name = type.__module__
954
+ module = sys.modules[module_name]
955
+
956
+ return module.__package__
957
+
958
+ def import_package(name: str):
959
+ """Import a package and all its submodules recursively."""
960
+ package = importlib.import_module(name)
961
+ results = {name: package}
812
962
 
813
- if env is Boot:
814
- add_provider(SingletonScope, SingletonScopeInstanceProvider())
815
- add_provider(RequestScope, RequestScopeInstanceProvider())
963
+ if hasattr(package, '__path__'): # it's a package, not a single file
964
+ for finder, name, ispkg in pkgutil.walk_packages(package.__path__, prefix=package.__name__ + "."):
965
+ try:
966
+ loaded = sys.modules
967
+
968
+ if loaded.get(name, None) is None:
969
+ Environment.logger.debug("import module %s", name)
970
+
971
+ submodule = importlib.import_module(name)
972
+ results[name] = submodule
973
+ else:
974
+ # skip import
975
+ results[name] = loaded[name]
976
+
977
+ except Exception as e:
978
+ Environment.logger.info("failed to import module %s due to %s", name, str(e))
979
+
980
+ return results
816
981
 
817
982
  def load_environment(env: Type):
818
983
  if env not in loaded:
@@ -826,48 +991,46 @@ class Environment:
826
991
  if decorator is None:
827
992
  raise DIRegistrationException(f"{env.__name__} is not an environment class")
828
993
 
829
- scan = env.__module__
830
- if "." in scan:
831
- scan = scan.rsplit('.', 1)[0]
994
+ # package
995
+
996
+ package_name = get_type_package(env)
832
997
 
833
998
  # recursion
834
999
 
835
1000
  for import_environment in decorator.args[0] or []:
836
1001
  load_environment(import_environment)
837
1002
 
838
- # load providers
1003
+ # import package
839
1004
 
840
- local_providers = {type: provider for type, provider in Providers.cache.items() if provider.get_module().startswith(scan)}
1005
+ if package_name is not None and len(package_name) > 0: # files outside of a package return None pr ""
1006
+ import_package(package_name)
841
1007
 
842
- # register providers
1008
+ # filter and load providers according to their module
843
1009
 
844
- # make sure, that for every type only a single EnvironmentInstanceProvider is created!
845
- # otherwise inheritance will fuck it up
1010
+ module_prefix = package_name
1011
+ if len(module_prefix) == 0:
1012
+ module_prefix = env.__module__
846
1013
 
847
- environment_providers : dict[AbstractInstanceProvider, EnvironmentInstanceProvider] = {}
1014
+ prefix_list.append(module_prefix)
848
1015
 
849
- for type, provider in local_providers.items():
850
- environment_provider = environment_providers.get(provider, None)
851
- if environment_provider is None:
852
- environment_provider = EnvironmentInstanceProvider(self, provider)
853
- environment_providers[provider] = environment_provider
1016
+ # go
854
1017
 
855
- add_provider(type, environment_provider)
1018
+ load_environment(env)
856
1019
 
857
- # tweak dependencies
1020
+ # filter according to the prefix list
858
1021
 
859
- for type, provider in local_providers.items():
860
- cast(EnvironmentInstanceProvider, self.providers[type]).tweak_dependencies(self.providers)
1022
+ def filter_provider(provider: AbstractInstanceProvider) -> bool:
1023
+ for prefix in prefix_list:
1024
+ if provider.get_host().__module__.startswith(prefix):
1025
+ return True
861
1026
 
862
- # return local providers
1027
+ return False
863
1028
 
864
- return environment_providers.values()
865
- else:
866
- return []
1029
+ self.providers.update(Providers.filter(self, filter_provider))
867
1030
 
868
1031
  # construct eager objects for local providers
869
1032
 
870
- for provider in load_environment(env):
1033
+ for provider in set(self.providers.values()):
871
1034
  if provider.is_eager():
872
1035
  provider.create(self)
873
1036
 
@@ -876,8 +1039,19 @@ class Environment:
876
1039
  for instance in self.instances:
877
1040
  self.execute_processors(Lifecycle.ON_RUNNING, instance)
878
1041
 
1042
+ def is_registered_type(self, type: Type) -> bool:
1043
+ provider = self.providers.get(type, None)
1044
+ return provider is not None and not isinstance(provider, AmbiguousProvider)
1045
+
1046
+ def registered_types(self, predicate: Callable[[Type], bool]) -> list[Type]:
1047
+ return [provider.get_type() for provider in self.providers.values()
1048
+ if predicate(provider.get_type())]
1049
+
879
1050
  # internal
880
1051
 
1052
+ def has_feature(self, feature: str) -> bool:
1053
+ return feature in self.features
1054
+
881
1055
  def execute_processors(self, lifecycle: Lifecycle, instance: T) -> T:
882
1056
  for processor in self.lifecycle_processors:
883
1057
  processor.process_lifecycle(lifecycle, instance, self)
@@ -927,8 +1101,9 @@ class Environment:
927
1101
 
928
1102
  builder.append("Providers \n")
929
1103
  for result_type, provider in self.providers.items():
930
- if cast(EnvironmentInstanceProvider, provider).environment is self:
931
- builder.append(f"- {result_type.__name__}: {cast(EnvironmentInstanceProvider, provider).provider}\n")
1104
+ if isinstance(provider, EnvironmentInstanceProvider):
1105
+ if cast(EnvironmentInstanceProvider, provider).environment is self:
1106
+ builder.append(f"- {result_type.__name__}: {provider.report()}\n")
932
1107
 
933
1108
  # instances
934
1109
 
@@ -973,6 +1148,9 @@ class Environment:
973
1148
 
974
1149
  return provider.create(self)
975
1150
 
1151
+ def __str__(self):
1152
+ return f"Environment({self.type.__name__})"
1153
+
976
1154
  class LifecycleCallable:
977
1155
  __slots__ = [
978
1156
  "decorator",
@@ -1155,20 +1333,20 @@ class InjectLifecycleCallable(LifecycleCallable):
1155
1333
  def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
1156
1334
  return [environment.get(type) for type in method.param_types]
1157
1335
 
1158
- def scope(name: str):
1336
+ def scope(name: str, register=True):
1159
1337
  def decorator(cls):
1160
1338
  Scopes.register(cls, name)
1161
1339
 
1162
1340
  Decorators.add(cls, scope)
1163
- # Decorators.add(cls, injectable)
1164
1341
 
1165
- Providers.register(ClassInstanceProvider(cls, eager=True, scope="request"))
1342
+ if register:
1343
+ Providers.register(ClassInstanceProvider(cls, eager=True, scope="request"))
1166
1344
 
1167
1345
  return cls
1168
1346
 
1169
1347
  return decorator
1170
1348
 
1171
- @scope("request")
1349
+ @scope("request", register=False)
1172
1350
  class RequestScope(Scope):
1173
1351
  # properties
1174
1352
 
@@ -1180,7 +1358,7 @@ class RequestScope(Scope):
1180
1358
  def get(self, provider: AbstractInstanceProvider, environment: Environment, arg_provider: Callable[[],list]):
1181
1359
  return provider.create(environment, *arg_provider())
1182
1360
 
1183
- @scope("singleton")
1361
+ @scope("singleton", register=False)
1184
1362
  class SingletonScope(Scope):
1185
1363
  # properties
1186
1364
 
@@ -1207,6 +1385,19 @@ class SingletonScope(Scope):
1207
1385
 
1208
1386
  return self.value
1209
1387
 
1388
+ @scope("environment", register=False)
1389
+ class EnvironmentScope(SingletonScope):
1390
+ # properties
1391
+
1392
+ __slots__ = [
1393
+ ]
1394
+
1395
+ # constructor
1396
+
1397
+ def __init__(self):
1398
+ super().__init__()
1399
+
1400
+
1210
1401
  @scope("thread")
1211
1402
  class ThreadScope(Scope):
1212
1403
  __slots__ = [