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/__init__.py ADDED
File without changes
aspyx/di/__init__.py CHANGED
@@ -1,14 +1,15 @@
1
1
  """
2
2
  This module provides dependency injection and aop capabilities for Python applications.
3
3
  """
4
- from .di import DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
4
+ from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
5
5
 
6
- # import something from the subpackages, so that teh decorators are executed
6
+ # import something from the subpackages, so that the decorators are executed
7
7
 
8
8
  from .configuration import ConfigurationManager
9
9
  from .aop import before
10
+ from .threading import SynchronizeAdvice
10
11
 
11
- imports = [ConfigurationManager, before]
12
+ imports = [ConfigurationManager, before, SynchronizeAdvice]
12
13
 
13
14
  __all__ = [
14
15
  "ClassInstanceProvider",
@@ -30,5 +31,8 @@ __all__ = [
30
31
  "AbstractCallableProcessor",
31
32
  "LifecycleCallable",
32
33
  "DIException",
33
- "Lifecycle"
34
+ "Lifecycle",
35
+ "conditional",
36
+ "requires_class",
37
+ "requires_feature"
34
38
  ]
aspyx/di/aop/aop.py CHANGED
@@ -12,10 +12,8 @@ from dataclasses import dataclass
12
12
  from enum import auto, Enum
13
13
  from typing import Optional, Dict, Type, Callable
14
14
 
15
- from aspyx.di.di import order
16
15
  from aspyx.reflection import Decorators, TypeDescriptor
17
- from aspyx.di import injectable, Providers, ClassInstanceProvider, Environment, PostProcessor
18
-
16
+ from aspyx.di import injectable, order, Providers, ClassInstanceProvider, Environment, PostProcessor
19
17
 
20
18
  class AOPException(Exception):
21
19
  """
@@ -50,7 +48,7 @@ class AspectTarget(ABC):
50
48
  __slots__ = [
51
49
  "_function",
52
50
  "_type",
53
-
51
+ "_async",
54
52
  "_clazz",
55
53
  "_instance",
56
54
  "names",
@@ -65,6 +63,7 @@ class AspectTarget(ABC):
65
63
  def __init__(self):
66
64
  self._clazz = None
67
65
  self._instance = None
66
+ self._async = False
68
67
  self._function = None
69
68
  self._type = None
70
69
 
@@ -108,11 +107,28 @@ class AspectTarget(ABC):
108
107
 
109
108
  return self
110
109
 
110
+ def that_are_async(self):
111
+ """
112
+ matches methods that are async
113
+ :return: self
114
+ """
115
+ self._async = True
116
+ return self
117
+
111
118
  def of_type(self, type: Type):
119
+ """
120
+ matches methods belonging to a class or classes that are subclasses of the specified type
121
+ :return: self
122
+ """
112
123
  self.types.append(type)
113
124
  return self
114
125
 
115
126
  def decorated_with(self, decorator):
127
+ """
128
+ matches methods or classes that are decorated with the specified decorator
129
+ :param decorator: the decorator callable
130
+ :return:
131
+ """
116
132
  self.decorators.append(decorator)
117
133
  return self
118
134
 
@@ -128,6 +144,9 @@ class AspectTarget(ABC):
128
144
  return self
129
145
 
130
146
  class ClassAspectTarget(AspectTarget):
147
+ """
148
+ An AspectTarget matching classes
149
+ """
131
150
  # properties
132
151
 
133
152
  __slots__ = [
@@ -167,6 +186,10 @@ class ClassAspectTarget(AspectTarget):
167
186
  # fluent
168
187
 
169
188
  class MethodAspectTarget(AspectTarget):
189
+ """
190
+ An AspectTarget matching methods
191
+ """
192
+
170
193
  # properties
171
194
 
172
195
  __slots__ = [ ]
@@ -178,6 +201,11 @@ class MethodAspectTarget(AspectTarget):
178
201
 
179
202
  method_descriptor = descriptor.get_method(func.__name__)
180
203
 
204
+ # async
205
+
206
+ if self._async is not method_descriptor.is_async():
207
+ return False
208
+
181
209
  # type
182
210
 
183
211
  if len(self.types) > 0:
@@ -219,14 +247,14 @@ def classes():
219
247
  return ClassAspectTarget()
220
248
 
221
249
 
222
- class JoinPoint:
250
+ class Aspect:
223
251
  __slots__ = [
224
252
  "next",
225
253
  ]
226
254
 
227
255
  # constructor
228
256
 
229
- def __init__(self, next: 'JoinPoint'):
257
+ def __init__(self, next: 'Aspect'):
230
258
  self.next = next
231
259
 
232
260
  # public
@@ -234,45 +262,61 @@ class JoinPoint:
234
262
  def call(self, invocation: 'Invocation'):
235
263
  pass
236
264
 
237
- class FunctionJoinPoint(JoinPoint):
265
+ async def call_async(self, invocation: 'Invocation'):
266
+ pass
267
+
268
+ class FunctionAspect(Aspect):
238
269
  __slots__ = [
239
270
  "instance",
240
271
  "func",
272
+ "order",
241
273
  ]
242
274
 
243
- def __init__(self, instance, func, next: Optional['JoinPoint']):
244
- super().__init__(next)
275
+ def __init__(self, instance, func, next_aspect: Optional['Aspect']):
276
+ super().__init__(next_aspect)
245
277
 
246
278
  self.instance = instance
247
279
  self.func = func
248
280
 
281
+ self.order = next((decorator.args[0] for decorator in Decorators.get(func) if decorator.decorator is order), 0)
282
+
249
283
  def call(self, invocation: 'Invocation'):
250
- invocation.current_join_point = self
284
+ invocation.current_aspect = self
251
285
 
252
286
  return self.func(self.instance, invocation)
253
287
 
254
- class MethodJoinPoint(FunctionJoinPoint):
288
+ async def call_async(self, invocation: 'Invocation'):
289
+ invocation.current_aspect = self
290
+
291
+ return await self.func(self.instance, invocation)
292
+
293
+ class MethodAspect(FunctionAspect):
255
294
  __slots__ = []
256
295
 
257
296
  def __init__(self, instance, func):
258
297
  super().__init__(instance, func, None)
259
298
 
260
299
  def call(self, invocation: 'Invocation'):
261
- invocation.current_join_point = self
300
+ invocation.current_aspect = self
262
301
 
263
302
  return self.func(*invocation.args, **invocation.kwargs)
264
303
 
304
+ async def call_async(self, invocation: 'Invocation'):
305
+ invocation.current_aspect = self
306
+
307
+ return await self.func(*invocation.args, **invocation.kwargs)
308
+
265
309
  @dataclass
266
- class JoinPoints:
267
- before: list[JoinPoint]
268
- around: list[JoinPoint]
269
- error: list[JoinPoint]
270
- after: list[JoinPoint]
310
+ class Aspects:
311
+ before: list[Aspect]
312
+ around: list[Aspect]
313
+ error: list[Aspect]
314
+ after: list[Aspect]
271
315
 
272
316
  class Invocation:
273
317
  """
274
318
  Invocation stores the relevant data of a single method invocation.
275
- It holds the arguments, keyword arguments, result, error, and the join points that define the aspect behavior.
319
+ It holds the arguments, keyword arguments, result, error, and the aspects that define the aspect behavior.
276
320
  """
277
321
  # properties
278
322
 
@@ -282,20 +326,20 @@ class Invocation:
282
326
  "kwargs",
283
327
  "result",
284
328
  "exception",
285
- "join_points",
286
- "current_join_point",
329
+ "aspects",
330
+ "current_aspect",
287
331
  ]
288
332
 
289
333
  # constructor
290
334
 
291
- def __init__(self, func, join_points: JoinPoints):
335
+ def __init__(self, func, aspects: Aspects):
292
336
  self.func = func
293
337
  self.args : list[object] = []
294
338
  self.kwargs = None
295
339
  self.result = None
296
340
  self.exception = None
297
- self.join_points = join_points
298
- self.current_join_point = None
341
+ self.aspects = aspects
342
+ self.current_aspect = None
299
343
 
300
344
  def call(self, *args, **kwargs):
301
345
  # remember args
@@ -305,23 +349,54 @@ class Invocation:
305
349
 
306
350
  # run all before
307
351
 
308
- for join_point in self.join_points.before:
309
- join_point.call(self)
352
+ for aspect in self.aspects.before:
353
+ aspect.call(self)
310
354
 
311
355
  # run around's with the method being the last aspect!
312
356
 
313
357
  try:
314
- self.result = self.join_points.around[0].call(self) # will follow the proceed chain
358
+ self.result = self.aspects.around[0].call(self) # will follow the proceed chain
315
359
 
316
360
  except Exception as e:
317
361
  self.exception = e
318
- for join_point in self.join_points.error:
319
- join_point.call(self)
362
+ for aspect in self.aspects.error:
363
+ aspect.call(self)
320
364
 
321
365
  # run all before
322
366
 
323
- for join_point in self.join_points.after:
324
- join_point.call(self)
367
+ for aspect in self.aspects.after:
368
+ aspect.call(self)
369
+
370
+ if self.exception is not None:
371
+ raise self.exception # rethrow the error
372
+
373
+ return self.result
374
+
375
+ async def call_async(self, *args, **kwargs):
376
+ # remember args
377
+
378
+ self.args = args
379
+ self.kwargs = kwargs
380
+
381
+ # run all before
382
+
383
+ for aspect in self.aspects.before:
384
+ aspect.call(self)
385
+
386
+ # run around's with the method being the last aspect!
387
+
388
+ try:
389
+ self.result = await self.aspects.around[0].call_async(self) # will follow the proceed chain
390
+
391
+ except Exception as e:
392
+ self.exception = e
393
+ for aspect in self.aspects.error:
394
+ aspect.call(self)
395
+
396
+ # run all before
397
+
398
+ for aspect in self.aspects.after:
399
+ aspect.call(self)
325
400
 
326
401
  if self.exception is not None:
327
402
  raise self.exception # rethrow the error
@@ -338,29 +413,41 @@ class Invocation:
338
413
 
339
414
  # next one please...
340
415
 
341
- return self.current_join_point.next.call(self)
416
+ return self.current_aspect.next.call(self)
417
+
418
+ async def proceed_async(self, *args, **kwargs):
419
+ """
420
+ Proceed to the next join point in the around chain up to the original method.
421
+ """
422
+ if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
423
+ self.args = args
424
+ self.kwargs = kwargs
425
+
426
+ # next one please...
427
+
428
+ return await self.current_aspect.next.call_async(self)
342
429
 
343
- @injectable()
344
- class Advice:
430
+ class Advices:
345
431
  # static data
346
432
 
347
433
  targets: list[AspectTarget] = []
348
434
 
349
- __slots__ = [
350
- "cache",
351
- "lock"
352
- ]
435
+ __slots__ = []
353
436
 
354
437
  # constructor
355
438
 
356
439
  def __init__(self):
357
- self.cache : Dict[Type, Dict[Callable,JoinPoints]] = {}
358
- self.lock = threading.RLock()
440
+ pass
359
441
 
360
442
  # methods
361
443
 
362
- def collect(self, clazz, member, type: AspectType, environment: Environment):
363
- aspects = [FunctionJoinPoint(environment.get(target._clazz), target._function, None) for target in Advice.targets if target._type == type and target._matches(clazz, member)]
444
+ @classmethod
445
+ def collect(cls, clazz, member, type: AspectType, environment: Environment):
446
+ aspects = [FunctionAspect(environment.get(target._clazz), target._function, None) for target in Advices.targets if target._type == type and target._clazz is not clazz and environment.providers.get(target._clazz) is not None and target._matches(clazz, member)]
447
+
448
+ # sort according to order
449
+
450
+ aspects = sorted(aspects, key=lambda aspect: aspect.order)
364
451
 
365
452
  # link
366
453
 
@@ -371,30 +458,23 @@ class Advice:
371
458
 
372
459
  return aspects
373
460
 
374
- def join_points4(self, instance, environment: Environment) -> Dict[Callable,JoinPoints]:
461
+ @classmethod
462
+ def aspects_for(cls, instance, environment: Environment) -> Dict[Callable,Aspects]:
375
463
  clazz = type(instance)
376
464
 
377
- result = self.cache.get(clazz, None)
378
- if result is None:
379
- with self.lock:
380
- result = self.cache.get(clazz, None)
381
-
382
- if result is None:
383
- result = {}
384
-
385
- for _, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
386
- join_points = self.compute_join_points(clazz, member, environment)
387
- if join_points is not None:
388
- result[member] = join_points
465
+ result = {}
389
466
 
390
- self.cache[clazz] = result
467
+ for _, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
468
+ aspects = cls.compute_aspects(clazz, member, environment)
469
+ if aspects is not None:
470
+ result[member] = aspects
391
471
 
392
472
  # add around methods
393
473
 
394
474
  value = {}
395
475
 
396
476
  for key, cjp in result.items():
397
- jp = JoinPoints(
477
+ jp = Aspects(
398
478
  before=cjp.before,
399
479
  around=cjp.around,
400
480
  error=cjp.error,
@@ -402,7 +482,7 @@ class Advice:
402
482
 
403
483
  # add method to around
404
484
 
405
- jp.around.append(MethodJoinPoint(instance, key))
485
+ jp.around.append(MethodAspect(instance, key))
406
486
  if len(jp.around) > 1:
407
487
  jp.around[len(jp.around) - 2].next = jp.around[len(jp.around) - 1]
408
488
 
@@ -412,14 +492,15 @@ class Advice:
412
492
 
413
493
  return value
414
494
 
415
- def compute_join_points(self, clazz, member, environment: Environment) -> Optional[JoinPoints]:
416
- befores = self.collect(clazz, member, AspectType.BEFORE, environment)
417
- arounds = self.collect(clazz, member, AspectType.AROUND, environment)
418
- afters = self.collect(clazz, member, AspectType.AFTER, environment)
419
- errors = self.collect(clazz, member, AspectType.ERROR, environment)
495
+ @classmethod
496
+ def compute_aspects(cls, clazz, member, environment: Environment) -> Optional[Aspects]:
497
+ befores = cls.collect(clazz, member, AspectType.BEFORE, environment)
498
+ arounds = cls.collect(clazz, member, AspectType.AROUND, environment)
499
+ afters = cls.collect(clazz, member, AspectType.AFTER, environment)
500
+ errors = cls.collect(clazz, member, AspectType.ERROR, environment)
420
501
 
421
502
  if len(befores) > 0 or len(arounds) > 0 or len(afters) > 0 or len(errors) > 0:
422
- return JoinPoints(
503
+ return Aspects(
423
504
  before=befores,
424
505
  around=arounds,
425
506
  error=errors,
@@ -437,7 +518,7 @@ def sanity_check(clazz: Type, name: str):
437
518
 
438
519
  def advice(cls):
439
520
  """
440
- Classes decorated with @advice are treated as aspect classes.
521
+ Classes decorated with @advice are treated as advice classes.
441
522
  They can contain methods decorated with @before, @after, @around, or @error to define aspects.
442
523
  """
443
524
  Providers.register(ClassInstanceProvider(cls, True))
@@ -447,10 +528,10 @@ def advice(cls):
447
528
  for name, member in TypeDescriptor.for_type(cls).methods.items():
448
529
  decorator = next((decorator for decorator in member.decorators if decorator.decorator in [before, after, around, error]), None)
449
530
  if decorator is not None:
450
- target = decorator.args[0]
531
+ target = decorator.args[0] # ? ...?? TODO, can be multiple
451
532
  target._clazz = cls
452
533
  sanity_check(cls, name)
453
- Advice.targets.append(target)
534
+ Advices.targets.append(target) #??
454
535
 
455
536
  return cls
456
537
 
@@ -512,31 +593,57 @@ def around(*targets: AspectTarget):
512
593
 
513
594
  return decorator
514
595
 
515
- @injectable()
596
+ @injectable(scope="environment")
516
597
  @order(0)
517
598
  class AdviceProcessor(PostProcessor):
518
599
  # properties
519
600
 
520
601
  __slots__ = [
521
- "advice",
602
+ "lock",
603
+ "cache"
522
604
  ]
523
605
 
524
606
  # constructor
525
607
 
526
- def __init__(self, advice: Advice):
608
+ def __init__(self):
527
609
  super().__init__()
528
610
 
529
- self.advice = advice
611
+ self.cache : Dict[Type, Dict[Callable,Aspects]] = {}
612
+ self.lock = threading.RLock()
613
+
614
+ # local
615
+
616
+ def aspects_for(self, instance, environment: Environment) -> Dict[Callable,Aspects]:
617
+ clazz = type(instance)
618
+ result = self.cache.get(clazz, None)
619
+ if result is None:
620
+ with self.lock:
621
+ result = Advices.aspects_for(instance, environment)
622
+ self.cache[clazz] = result # TOID der cache ist zu dick?????
623
+
624
+ return result
530
625
 
531
626
  # implement
532
627
 
533
628
  def process(self, instance: object, environment: Environment):
534
- join_point_dict = self.advice.join_points4(instance, environment)
629
+ aspect_dict = self.aspects_for(instance, environment)
535
630
 
536
- for member, joinPoints in join_point_dict.items():
631
+ for member, aspects in aspect_dict.items():
537
632
  Environment.logger.debug("add aspects for %s:%s", type(instance), member.__name__)
538
633
 
539
634
  def wrap(jp):
540
- return lambda *args, **kwargs: Invocation(member, jp).call(*args, **kwargs)
635
+ def sync_wrapper(*args, **kwargs):
636
+ return Invocation(member, jp).call(*args, **kwargs)
637
+
638
+ return sync_wrapper
639
+
640
+ def wrap_async(jp):
641
+ async def async_wrapper(*args, **kwargs):
642
+ return await Invocation(member, jp).call_async(*args, **kwargs)
643
+
644
+ return async_wrapper
541
645
 
542
- setattr(instance, member.__name__, types.MethodType(wrap(joinPoints), instance))
646
+ if inspect.iscoroutinefunction(member):
647
+ setattr(instance, member.__name__, types.MethodType(wrap_async(aspects), instance))
648
+ else:
649
+ setattr(instance, member.__name__, types.MethodType(wrap(aspects), instance))
@@ -52,4 +52,4 @@ class EnvConfigurationSource(ConfigurationSource):
52
52
  else:
53
53
  exploded[key] = value
54
54
 
55
- return exploded
55
+ return exploded
@@ -23,4 +23,4 @@ class YamlConfigurationSource(ConfigurationSource):
23
23
 
24
24
  def load(self) -> dict:
25
25
  with open(self.file, "r") as file:
26
- return yaml.safe_load(file)
26
+ return yaml.safe_load(file)