aspyx 1.4.0__py3-none-any.whl → 1.5.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
@@ -62,33 +62,76 @@ class DIRuntimeException(DIException):
62
62
 
63
63
  class AbstractInstanceProvider(ABC, Generic[T]):
64
64
  """
65
- Interface for instance providers.
65
+ An AbstractInstanceProvider is responsible to create instances.
66
66
  """
67
67
  @abstractmethod
68
68
  def get_module(self) -> str:
69
- pass
69
+ """
70
+ return the module name of the provider
71
+
72
+ Returns:
73
+ str: the module name of the provider
74
+ """
70
75
 
71
76
  def get_host(self) -> Type[T]:
77
+ """
78
+ return the class which is responsible for creation ( e.g. the injectable class )
79
+
80
+ Returns:
81
+ Type[T]: the class which is responsible for creation
82
+ """
72
83
  return type(self)
73
84
 
74
85
  @abstractmethod
75
86
  def get_type(self) -> Type[T]:
76
- pass
87
+ """
88
+ return the type of the created instance
89
+
90
+ Returns:
91
+ Type[T: the type]
92
+ """
77
93
 
78
94
  @abstractmethod
79
95
  def is_eager(self) -> bool:
80
- pass
96
+ """
97
+ return True, if the provider will eagerly construct instances
98
+
99
+ Returns:
100
+ bool: eager flag
101
+ """
81
102
 
82
103
  @abstractmethod
83
104
  def get_scope(self) -> str:
84
- pass
105
+ """
106
+ return the scope name
107
+
108
+ Returns:
109
+ str: the scope name
110
+ """
111
+
85
112
 
86
113
  def get_dependencies(self) -> (list[Type],int):
114
+ """
115
+ return the types that i depend on ( for constructor or setter injection ).
116
+ The second tuple element is the number of parameters that a construction injection will require
117
+
118
+ Returns:
119
+ (list[Type],int): the type array and the number of parameters
120
+ """
87
121
  return [],1
88
122
 
89
123
  @abstractmethod
90
- def create(self, environment: Environment, *args):
91
- pass
124
+ def create(self, environment: Environment, *args) -> T:
125
+ """
126
+ Create a new instance.
127
+
128
+ Args:
129
+ environment: the Environment
130
+ *args: the required arguments
131
+
132
+ Returns:
133
+ T: the instance
134
+ """
92
135
 
93
136
  def report(self) -> str:
94
137
  return str(self)
@@ -163,7 +206,7 @@ class SingletonScopeInstanceProvider(InstanceProvider):
163
206
 
164
207
  class EnvironmentScopeInstanceProvider(InstanceProvider):
165
208
  def __init__(self):
166
- super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request") # TODO?
209
+ super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request")
167
210
 
168
211
  def create(self, environment: Environment, *args):
169
212
  return EnvironmentScope()
@@ -273,13 +316,32 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
273
316
 
274
317
  self.environment = environment
275
318
  self.provider = provider
276
- self.dependencies = []
319
+ self.dependencies : list[AbstractInstanceProvider] = []
277
320
  self.scope_instance = Scopes.get(provider.get_scope(), environment)
278
321
 
322
+ # public
323
+
324
+ def print_tree(self, prefix=""):
325
+ children = self.dependencies
326
+ last_index = len(children) - 1
327
+ print(prefix + "+- " + self.report())
328
+
329
+ for i, child in enumerate(children):
330
+ if i == last_index:
331
+ # Last child
332
+ child_prefix = prefix + " "
333
+ else:
334
+ # Not last child
335
+ child_prefix = prefix + "| "
336
+
337
+
338
+ cast(EnvironmentInstanceProvider, child).print_tree(child_prefix)
339
+
279
340
  # implement
280
341
 
281
342
  def resolve(self, context: Providers.ResolveContext):
282
343
  context.add(self)
344
+ context.push(self)
283
345
 
284
346
  if not context.is_resolved(self):
285
347
  context.provider_dependencies[self] = [] #?
@@ -289,7 +351,7 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
289
351
  for type in type_and_params[0]:
290
352
  if params > 0:
291
353
  params -= 1
292
- self.dependencies.append(context.get_provider(type))
354
+ self.dependencies.append(context.get_provider(type)) # try/catch TODO
293
355
 
294
356
  provider = context.add_provider_dependency(self, type)
295
357
  if provider is not None:
@@ -298,6 +360,8 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
298
360
  else:
299
361
  context.add(*context.get_provider_dependencies(self))
300
362
 
363
+ context.pop()
364
+
301
365
  def get_module(self) -> str:
302
366
  return self.provider.get_module()
303
367
 
@@ -360,9 +424,13 @@ class ClassInstanceProvider(InstanceProvider):
360
424
  for method in TypeDescriptor.for_type(self.type).get_methods():
361
425
  if method.has_decorator(inject):
362
426
  for param in method.param_types:
427
+ if not Providers.is_registered(param):
428
+ raise DIRegistrationException(f"{self.type.__name__}.{method.method.__name__} declares an unknown parameter type {param.__name__}")
429
+
363
430
  types.append(param)
431
+ # done
364
432
 
365
- return (types, self.params)
433
+ return types, self.params
366
434
 
367
435
  def create(self, environment: Environment, *args):
368
436
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
@@ -388,28 +456,28 @@ class FunctionInstanceProvider(InstanceProvider):
388
456
 
389
457
  # constructor
390
458
 
391
- def __init__(self, clazz : Type, method, return_type : Type[T], eager = True, scope = "singleton"):
392
- super().__init__(clazz, return_type, eager, scope)
459
+ def __init__(self, clazz : Type, method: TypeDescriptor.MethodDescriptor, eager = True, scope = "singleton"):
460
+ super().__init__(clazz, method.return_type, eager, scope)
393
461
 
394
- self.method = method
462
+ self.method : TypeDescriptor.MethodDescriptor = method
395
463
 
396
464
  # implement
397
465
 
398
466
  def get_dependencies(self) -> (list[Type],int):
399
- return [self.host], 1
467
+ return [self.host, *self.method.param_types], 1 + len(self.method.param_types)
400
468
 
401
469
  def create(self, environment: Environment, *args):
402
470
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
403
471
 
404
- instance = self.method(*args) # args[0]=self
472
+ instance = self.method.method(*args) # args[0]=self
405
473
 
406
474
  return environment.created(instance)
407
475
 
408
476
  def report(self) -> str:
409
- return f"{self.host.__name__}.{self.method.__name__}"
477
+ return f"{self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__qualname__}"
410
478
 
411
479
  def __str__(self):
412
- return f"FunctionInstanceProvider({self.host.__name__}.{self.method.__name__} -> {self.type.__name__})"
480
+ return f"FunctionInstanceProvider({self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__name__})"
413
481
 
414
482
  class FactoryInstanceProvider(InstanceProvider):
415
483
  """
@@ -440,7 +508,7 @@ class FactoryInstanceProvider(InstanceProvider):
440
508
  return environment.created(args[0].create())
441
509
 
442
510
  def report(self) -> str:
443
- return f"{self.host.__name__}.create"
511
+ return f"{self.host.__name__}.create() -> {self.type.__name__} "
444
512
 
445
513
  def __str__(self):
446
514
  return f"FactoryInstanceProvider({self.host.__name__} -> {self.type.__name__})"
@@ -449,6 +517,12 @@ class FactoryInstanceProvider(InstanceProvider):
449
517
  class Lifecycle(Enum):
450
518
  """
451
519
  This enum defines the lifecycle phases that can be processed by lifecycle processors.
520
+ Phases are:
521
+
522
+ - ON_INJECT
523
+ - ON_INIT
524
+ - ON_RUNNING
525
+ - ON_DESTROY
452
526
  """
453
527
 
454
528
  __slots__ = []
@@ -504,7 +578,8 @@ class Providers:
504
578
  __slots__ = [
505
579
  "dependencies",
506
580
  "providers",
507
- "provider_dependencies"
581
+ "provider_dependencies",
582
+ "path"
508
583
  ]
509
584
 
510
585
  # constructor
@@ -512,6 +587,7 @@ class Providers:
512
587
  def __init__(self, providers: Dict[Type, EnvironmentInstanceProvider]):
513
588
  self.dependencies : list[EnvironmentInstanceProvider] = []
514
589
  self.providers = providers
590
+ self.path = []
515
591
  self.provider_dependencies : dict[EnvironmentInstanceProvider, list[EnvironmentInstanceProvider]] = {}
516
592
 
517
593
  # public
@@ -537,7 +613,14 @@ class Providers:
537
613
 
538
614
  return provider
539
615
 
616
+ def push(self, provider):
617
+ self.path.append(provider)
618
+
619
+ def pop(self):
620
+ self.path.pop()
621
+
540
622
  def next(self):
623
+ self.path.clear()
541
624
  self.dependencies.clear()
542
625
 
543
626
  def get_provider(self, type: Type) -> EnvironmentInstanceProvider:
@@ -558,7 +641,7 @@ class Providers:
558
641
  cycle = ""
559
642
 
560
643
  first = True
561
- for p in self.dependencies:
644
+ for p in self.path:
562
645
  if not first:
563
646
  cycle += " -> "
564
647
 
@@ -589,6 +672,10 @@ class Providers:
589
672
  else:
590
673
  candidates.append(provider)
591
674
 
675
+ @classmethod
676
+ def is_registered(cls,type: Type) -> bool:
677
+ return Providers.providers.get(type, None) is not None
678
+
592
679
  # add factories lazily
593
680
 
594
681
  @classmethod
@@ -603,7 +690,7 @@ class Providers:
603
690
  cache: Dict[Type,AbstractInstanceProvider] = {}
604
691
 
605
692
  context: ConditionContext = {
606
- "requires_feature": lambda feature : environment.has_feature(feature),
693
+ "requires_feature": environment.has_feature,
607
694
  "requires_class": lambda clazz : cache.get(clazz, None) is not None # ? only works if the class is in the cache already?
608
695
  }
609
696
 
@@ -659,10 +746,13 @@ class Providers:
659
746
  if type is provider.get_type():
660
747
  raise ProviderCollisionException(f"type {type.__name__} already registered", existing_provider, provider)
661
748
 
662
- if isinstance(existing_provider, AmbiguousProvider):
663
- cast(AmbiguousProvider, existing_provider).add_provider(provider)
664
- else:
665
- cache[type] = AmbiguousProvider(type, existing_provider, provider)
749
+ if existing_provider.get_type() is not type:
750
+ # only overwrite if the existing provider is not specific
751
+
752
+ if isinstance(existing_provider, AmbiguousProvider):
753
+ cast(AmbiguousProvider, existing_provider).add_provider(provider)
754
+ else:
755
+ cache[type] = AmbiguousProvider(type, existing_provider, provider)
666
756
 
667
757
  # recursion
668
758
 
@@ -714,8 +804,7 @@ def register_factories(cls: Type):
714
804
  if return_type is None:
715
805
  raise DIRegistrationException(f"{cls.__name__}.{method.method.__name__} expected to have a return type")
716
806
 
717
- Providers.register(FunctionInstanceProvider(cls, method.method, return_type, create_decorator.args[0],
718
- create_decorator.args[1]))
807
+ Providers.register(FunctionInstanceProvider(cls, method, create_decorator.args[0], create_decorator.args[1]))
719
808
  def order(prio = 0):
720
809
  def decorator(cls):
721
810
  Decorators.add(cls, order, prio)
@@ -740,6 +829,10 @@ def injectable(eager=True, scope="singleton"):
740
829
  def factory(eager=True, scope="singleton"):
741
830
  """
742
831
  Decorator that needs to be used on a class that implements the Factory interface.
832
+
833
+ Args:
834
+ eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
835
+ scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
743
836
  """
744
837
  def decorator(cls):
745
838
  Decorators.add(cls, factory)
@@ -754,6 +847,10 @@ def factory(eager=True, scope="singleton"):
754
847
  def create(eager=True, scope="singleton"):
755
848
  """
756
849
  Any method annotated with @create will be registered as a factory method.
850
+
851
+ Args:
852
+ eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
853
+ scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
757
854
  """
758
855
  def decorator(func):
759
856
  Decorators.add(func, create, eager, scope)
@@ -763,7 +860,7 @@ def create(eager=True, scope="singleton"):
763
860
 
764
861
  def on_init():
765
862
  """
766
- Methods annotated with @on_init will be called when the instance is created."""
863
+ Methods annotated with `@on_init` will be called when the instance is created."""
767
864
  def decorator(func):
768
865
  Decorators.add(func, on_init)
769
866
  return func
@@ -772,7 +869,7 @@ def on_init():
772
869
 
773
870
  def on_running():
774
871
  """
775
- Methods annotated with @on_running will be called when the container up and running."""
872
+ Methods annotated with `@on_running` will be called when the container up and running."""
776
873
  def decorator(func):
777
874
  Decorators.add(func, on_running)
778
875
  return func
@@ -781,7 +878,7 @@ def on_running():
781
878
 
782
879
  def on_destroy():
783
880
  """
784
- Methods annotated with @on_destroy will be called when the instance is destroyed.
881
+ Methods annotated with `@on_destroy` will be called when the instance is destroyed.
785
882
  """
786
883
  def decorator(func):
787
884
  Decorators.add(func, on_destroy)
@@ -789,18 +886,19 @@ def on_destroy():
789
886
 
790
887
  return decorator
791
888
 
792
- def environment(imports: Optional[list[Type]] = None):
889
+ def module(imports: Optional[list[Type]] = None):
793
890
  """
794
- This annotation is used to mark classes that control the set of injectables that will be managed based on their location
795
- relative to the module of the class. All @injectable s and @factory s that are located in the same or any sub-module will
891
+ This annotation is used to mark classes that control the discovery process of injectables based on their location
892
+ relative to the module of the class. All `@injectable`s and `@factory`s that are located in the same or any sub-module will
796
893
  be registered and managed accordingly.
797
- Arguments:
798
- imports (Optional[list[Type]]): Optional list of imported environment types
894
+
895
+ Args:
896
+ imports (Optional[list[Type]]): Optional list of imported module types
799
897
  """
800
898
  def decorator(cls):
801
899
  Providers.register(ClassInstanceProvider(cls, True))
802
900
 
803
- Decorators.add(cls, environment, imports)
901
+ Decorators.add(cls, module, imports)
804
902
  Decorators.add(cls, injectable) # do we need that?
805
903
 
806
904
  return cls
@@ -873,11 +971,28 @@ def conditional(*conditions: Condition):
873
971
  class Environment:
874
972
  """
875
973
  Central class that manages the lifecycle of instances and their dependencies.
974
+
975
+ Usage:
976
+
977
+ ```python
978
+ @injectable()
979
+ class Foo:
980
+ def __init__(self):
981
+
982
+ @module()
983
+ class Module:
984
+ def __init__(self):
985
+ pass
986
+
987
+ environment = Environment(Module)
988
+
989
+ foo = environment.get(Foo) # will create an instance of Foo
990
+ ```
876
991
  """
877
992
 
878
993
  # static data
879
994
 
880
- logger = logging.getLogger(__name__) # __name__ = module name
995
+ logger = logging.getLogger("aspyx.di") # __name__ = module name
881
996
 
882
997
  instance : 'Environment' = None
883
998
 
@@ -951,9 +1066,33 @@ class Environment:
951
1066
 
952
1067
  def get_type_package(type: Type):
953
1068
  module_name = type.__module__
954
- module = sys.modules[module_name]
1069
+ module = sys.modules.get(module_name)
1070
+
1071
+ if not module:
1072
+ raise ImportError(f"Module {module_name} not found")
1073
+
1074
+ # Try to get the package
1075
+
1076
+ package = getattr(module, '__package__', None)
955
1077
 
956
- return module.__package__
1078
+ # Fallback: if module is __main__, try to infer from the module name if possible
1079
+
1080
+ if not package:
1081
+ if module_name == '__main__':
1082
+ # Try to resolve real name via __file__
1083
+ path = getattr(module, '__file__', None)
1084
+ if path:
1085
+ Environment.logger.warning(
1086
+ "Module is __main__; consider running via -m to preserve package context")
1087
+ return ''
1088
+
1089
+ # Try to infer package name from module name
1090
+
1091
+ parts = module_name.split('.')
1092
+ if len(parts) > 1:
1093
+ return '.'.join(parts[:-1])
1094
+
1095
+ return package or ''
957
1096
 
958
1097
  def import_package(name: str):
959
1098
  """Import a package and all its submodules recursively."""
@@ -987,7 +1126,7 @@ class Environment:
987
1126
 
988
1127
  # sanity check
989
1128
 
990
- decorator = TypeDescriptor.for_type(env).get_decorator(environment)
1129
+ decorator = TypeDescriptor.for_type(env).get_decorator(module)
991
1130
  if decorator is None:
992
1131
  raise DIRegistrationException(f"{env.__name__} is not an environment class")
993
1132
 
@@ -1031,6 +1170,9 @@ class Environment:
1031
1170
  # construct eager objects for local providers
1032
1171
 
1033
1172
  for provider in set(self.providers.values()):
1173
+ if isinstance(provider, EnvironmentInstanceProvider):
1174
+ provider.print_tree()
1175
+
1034
1176
  if provider.is_eager():
1035
1177
  provider.create(self)
1036
1178
 
@@ -1136,10 +1278,11 @@ class Environment:
1136
1278
  """
1137
1279
  Create or return a cached instance for the given type.
1138
1280
 
1139
- Arguments:
1281
+ Args:
1140
1282
  type (Type): The desired type
1141
1283
 
1142
- Returns: The requested instance
1284
+ Returns:
1285
+ T: The requested instance
1143
1286
  """
1144
1287
  provider = self.providers.get(type, None)
1145
1288
  if provider is None:
@@ -1413,11 +1556,12 @@ class ThreadScope(Scope):
1413
1556
  def get(self, provider: AbstractInstanceProvider, environment: Environment, arg_provider: Callable[[], list]):
1414
1557
  if not hasattr(self._local, "value"):
1415
1558
  self._local.value = provider.create(environment, *arg_provider())
1559
+
1416
1560
  return self._local.value
1417
1561
 
1418
1562
  # internal class that is required to import technical instance providers
1419
1563
 
1420
- @environment()
1564
+ @module()
1421
1565
  class Boot:
1422
1566
  # class
1423
1567
 
@@ -1,5 +1,5 @@
1
1
  """
2
- This module provides utility functions.
2
+ This module provides exception handling functions.
3
3
  """
4
4
  from .exception_manager import exception_handler, handle, ExceptionManager
5
5
 
@@ -32,9 +32,6 @@ def handle():
32
32
 
33
33
  return decorator
34
34
 
35
- class ErrorContext():
36
- pass
37
-
38
35
  class Handler:
39
36
  # constructor
40
37
 
@@ -43,8 +40,13 @@ class Handler:
43
40
  self.instance = instance
44
41
  self.handler = handler
45
42
 
46
- def handle(self, exception: BaseException):
47
- self.handler(self.instance, exception)
43
+ def handle(self, exception: BaseException) -> BaseException:
44
+ result = self.handler(self.instance, exception)
45
+
46
+ if result is not None:
47
+ return result
48
+ else:
49
+ return exception
48
50
 
49
51
  class Chain:
50
52
  # constructor
@@ -55,11 +57,11 @@ class Chain:
55
57
 
56
58
  # public
57
59
 
58
- def handle(self, exception: BaseException):
59
- self.handler.handle(exception)
60
+ def handle(self, exception: BaseException) -> BaseException:
61
+ return self.handler.handle(exception)
60
62
 
61
63
  class Invocation:
62
- def __init__(self, exception: Exception, chain: Chain):
64
+ def __init__(self, exception: BaseException, chain: Chain):
63
65
  self.exception = exception
64
66
  self.chain = chain
65
67
  self.current = self.chain
@@ -74,17 +76,26 @@ class ExceptionManager:
74
76
 
75
77
  exception_handler_classes = []
76
78
 
77
- invocation = ThreadLocal()
79
+ invocation = ThreadLocal[Invocation]()
78
80
 
79
81
  # class methods
80
82
 
81
83
  @classmethod
82
- def proceed(cls):
84
+ def proceed(cls) -> BaseException:
85
+ """
86
+ proceed with the next most applicable handler
87
+
88
+ Returns:
89
+ BaseException: the resulting exception
90
+
91
+ """
83
92
  invocation = cls.invocation.get()
84
93
 
85
94
  invocation.current = invocation.current.next
86
95
  if invocation.current is not None:
87
- invocation.current.handle(invocation.exception)
96
+ return invocation.current.handle(invocation.exception)
97
+ else:
98
+ return invocation.exception
88
99
 
89
100
  # constructor
90
101
 
@@ -93,7 +104,6 @@ class ExceptionManager:
93
104
  self.handler : list[Handler] = []
94
105
  self.cache: Dict[Type, Chain] = {}
95
106
  self.lock = RLock()
96
- self.current_context: Optional[ErrorContext] = None
97
107
 
98
108
  # internal
99
109
 
@@ -145,7 +155,7 @@ class ExceptionManager:
145
155
 
146
156
  # chain
147
157
 
148
- for i in range(0, len(chain) - 2):
158
+ for i in range(0, len(chain) - 1):
149
159
  chain[i].next = chain[i + 1]
150
160
 
151
161
  if len(chain) > 0:
@@ -153,16 +163,23 @@ class ExceptionManager:
153
163
  else:
154
164
  return None
155
165
 
156
- def handle(self, exception: Exception):
166
+ def handle(self, exception: BaseException) -> BaseException:
157
167
  """
158
- handle an exception by invoking the most applicable handler ( according to mro )
159
- :param exception: the exception
168
+ handle an exception by invoking the most applicable handler (according to mro)
169
+ and return a possible modified exception as a result.
170
+
171
+ Args:
172
+ exception (BaseException): the exception
173
+
174
+ Returns:
175
+ BaseException: the resulting exception
160
176
  """
161
177
  chain = self.get_handlers(type(exception))
162
178
  if chain is not None:
163
-
164
179
  self.invocation.set(Invocation(exception, chain))
165
180
  try:
166
- chain.handle(exception)
181
+ return chain.handle(exception)
167
182
  finally:
168
183
  self.invocation.clear()
184
+ else:
185
+ return exception # hmmm?
aspyx/reflection/proxy.py CHANGED
@@ -1,7 +1,8 @@
1
1
  """
2
2
  Dynamic proxies for method interception and delegation.
3
3
  """
4
- from typing import Generic, TypeVar, Type
4
+ import inspect
5
+ from typing import Generic, TypeVar, Type, Callable
5
6
 
6
7
  T = TypeVar("T")
7
8
 
@@ -14,6 +15,7 @@ class DynamicProxy(Generic[T]):
14
15
  by intercepting method calls at runtime and handling them as needed.
15
16
 
16
17
  Usage:
18
+ ```python
17
19
  class MyHandler(DynamicProxy.InvocationHandler):
18
20
  def invoke(self, invocation):
19
21
  print(f"Intercepted: {invocation.name}")
@@ -22,17 +24,23 @@ class DynamicProxy(Generic[T]):
22
24
 
23
25
  proxy = DynamicProxy.create(SomeClass, MyHandler())
24
26
  proxy.some_method(args) # Will be intercepted by MyHandler.invoke
25
-
26
- Attributes:
27
- type: The proxied class type.
28
- invocation_handler: The handler that processes intercepted method calls.
27
+ ```
29
28
  """
30
29
  # inner class
31
30
 
32
31
  class Invocation:
33
- def __init__(self, type: Type[T], name: str, *args, **kwargs):
32
+ __slots__ = [
33
+ "type",
34
+ "method",
35
+ "args",
36
+ "kwargs",
37
+ ]
38
+
39
+ # constructor
40
+
41
+ def __init__(self, type: Type[T], method: Callable, *args, **kwargs):
34
42
  self.type = type
35
- self.name = name
43
+ self.method = method
36
44
  self.args = args
37
45
  self.kwargs = kwargs
38
46
 
@@ -40,12 +48,20 @@ class DynamicProxy(Generic[T]):
40
48
  def invoke(self, invocation: 'DynamicProxy.Invocation'):
41
49
  pass
42
50
 
51
+ async def invoke_async(self, invocation: 'DynamicProxy.Invocation'):
52
+ return self.invoke(invocation)
53
+
43
54
  # class methods
44
55
 
45
56
  @classmethod
46
57
  def create(cls, type: Type[T], invocation_handler: 'DynamicProxy.InvocationHandler') -> T:
47
58
  return DynamicProxy(type, invocation_handler)
48
59
 
60
+ __slots__ = [
61
+ "type",
62
+ "invocation_handler"
63
+ ]
64
+
49
65
  # constructor
50
66
 
51
67
  def __init__(self, type: Type[T], invocation_handler: 'DynamicProxy.InvocationHandler'):
@@ -55,7 +71,16 @@ class DynamicProxy(Generic[T]):
55
71
  # public
56
72
 
57
73
  def __getattr__(self, name):
58
- def wrapper(*args, **kwargs):
59
- return self.invocation_handler.invoke(DynamicProxy.Invocation(self.type, name, *args, **kwargs))
74
+ method = getattr(self.type, name)
75
+
76
+ if inspect.iscoroutinefunction(method):
77
+ async def async_wrapper(*args, **kwargs):
78
+ return await self.invocation_handler.invoke_async(DynamicProxy.Invocation(self.type, method, *args, **kwargs))
79
+
80
+ return async_wrapper
81
+
82
+ else:
83
+ def sync_wrapper(*args, **kwargs):
84
+ return self.invocation_handler.invoke(DynamicProxy.Invocation(self.type, method, *args, **kwargs))
60
85
 
61
- return wrapper
86
+ return sync_wrapper