aspyx 1.3.0__py3-none-any.whl → 1.4.1__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,7 +1,7 @@
1
1
  """
2
2
  This module provides dependency injection and aop capabilities for Python applications.
3
3
  """
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
4
+ from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
5
5
 
6
6
  # import something from the subpackages, so that the decorators are executed
7
7
 
@@ -17,7 +17,7 @@ __all__ = [
17
17
  "Environment",
18
18
  "injectable",
19
19
  "factory",
20
- "environment",
20
+ "module",
21
21
  "inject",
22
22
  "create",
23
23
  "order",
aspyx/di/aop/__init__.py CHANGED
@@ -1,7 +1,20 @@
1
1
  """
2
- AOP module
2
+ The AOP module gives you the possibility to define aspects that will participate in method execution flows.
3
+
4
+ **Example**: all method executions of methods named "foo" will include a `before` aspect, that will be executed before the original method
5
+
6
+ ```python
7
+ @advice
8
+ class Advice:
9
+ @before(methods().named("foo"))
10
+ def before_call(self, invocation: Invocation):
11
+ ...
12
+
13
+ ```
14
+
15
+ Note, that this requires that both the advice and the targeted methods need to be managed by an environment.
3
16
  """
4
- from .aop import before, after, classes, around, error, advice, methods, Invocation
17
+ from .aop import before, after, classes, around, error, advice, methods, Invocation, AspectTarget
5
18
  __all__ = [
6
19
  "before",
7
20
  "after",
@@ -11,4 +24,5 @@ __all__ = [
11
24
  "classes",
12
25
  "methods",
13
26
  "Invocation",
27
+ "AspectTarget"
14
28
  ]
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
  """
@@ -27,6 +25,7 @@ class AspectType(Enum):
27
25
  AspectType defines the types of aspect-oriented advice that can be applied to methods.
28
26
 
29
27
  The available types are:
28
+
30
29
  - BEFORE: Advice to be executed before the method invocation.
31
30
  - AROUND: Advice that intercepts the method invocation.
32
31
  - AFTER: Advice to be executed after the method invocation, regardless of its outcome.
@@ -100,7 +99,7 @@ class AspectTarget(ABC):
100
99
 
101
100
  # fluent
102
101
 
103
- def function(self, func):
102
+ def function(self, func) -> AspectTarget:
104
103
  self._function = func
105
104
  return self
106
105
 
@@ -109,30 +108,72 @@ class AspectTarget(ABC):
109
108
 
110
109
  return self
111
110
 
112
- def that_are_async(self):
111
+ def that_are_async(self) -> AspectTarget:
112
+ """
113
+ matches methods that are async
114
+
115
+ Returns:
116
+ AspectTarget: self
117
+ """
113
118
  self._async = True
114
119
  return self
115
120
 
116
- def of_type(self, type: Type):
121
+ def of_type(self, type: Type) -> AspectTarget:
122
+ """
123
+ matches methods belonging to a class or classes that are subclasses of the specified type
124
+
125
+ Args:
126
+ type (Type): the type to match against
127
+
128
+ Returns:
129
+ AspectTarget: self
130
+ """
117
131
  self.types.append(type)
118
132
  return self
119
133
 
120
- def decorated_with(self, decorator):
134
+ def decorated_with(self, decorator: Callable) -> AspectTarget:
135
+ """
136
+ matches methods or classes that are decorated with the specified decorator
137
+
138
+ Args:
139
+ decorator (Callable): the decorator callable
140
+
141
+ Returns:
142
+ AspectTarget: self
143
+ """
121
144
  self.decorators.append(decorator)
122
145
  return self
123
146
 
124
- def matches(self, pattern: str):
147
+ def matches(self, pattern: str) -> AspectTarget:
125
148
  """
126
149
  Matches the target against a pattern.
150
+
151
+ Args:
152
+ pattern (str): the pattern
153
+
154
+ Returns:
155
+ AspectTarget: self
127
156
  """
128
157
  self.patterns.append(re.compile(pattern))
129
158
  return self
130
159
 
131
- def named(self, name: str):
160
+ def named(self, name: str) -> AspectTarget:
161
+ """
162
+ Matches the target against a name.
163
+
164
+ Args:
165
+ name (str): the name
166
+
167
+ Returns:
168
+ AspectTarget: self
169
+ """
132
170
  self.names.append(name)
133
171
  return self
134
172
 
135
173
  class ClassAspectTarget(AspectTarget):
174
+ """
175
+ An AspectTarget matching classes
176
+ """
136
177
  # properties
137
178
 
138
179
  __slots__ = [
@@ -172,6 +213,10 @@ class ClassAspectTarget(AspectTarget):
172
213
  # fluent
173
214
 
174
215
  class MethodAspectTarget(AspectTarget):
216
+ """
217
+ An AspectTarget matching methods
218
+ """
219
+
175
220
  # properties
176
221
 
177
222
  __slots__ = [ ]
@@ -185,9 +230,6 @@ class MethodAspectTarget(AspectTarget):
185
230
 
186
231
  # async
187
232
 
188
- name = method_descriptor.method.__name__
189
- is_async = method_descriptor.is_async()
190
-
191
233
  if self._async is not method_descriptor.is_async():
192
234
  return False
193
235
 
@@ -219,27 +261,33 @@ class MethodAspectTarget(AspectTarget):
219
261
 
220
262
  return True
221
263
 
222
- def methods():
264
+ def methods() -> AspectTarget:
223
265
  """
224
266
  Create a new AspectTarget instance to define method aspect targets.
267
+
268
+ Returns:
269
+ AspectTarget: the method target
225
270
  """
226
271
  return MethodAspectTarget()
227
272
 
228
- def classes():
273
+ def classes() -> AspectTarget:
229
274
  """
230
275
  Create a new AspectTarget instance to define class aspect targets.
276
+
277
+ Returns:
278
+ AspectTarget: the method target
231
279
  """
232
280
  return ClassAspectTarget()
233
281
 
234
282
 
235
- class JoinPoint:
283
+ class Aspect:
236
284
  __slots__ = [
237
285
  "next",
238
286
  ]
239
287
 
240
288
  # constructor
241
289
 
242
- def __init__(self, next: 'JoinPoint'):
290
+ def __init__(self, next: 'Aspect'):
243
291
  self.next = next
244
292
 
245
293
  # public
@@ -250,55 +298,58 @@ class JoinPoint:
250
298
  async def call_async(self, invocation: 'Invocation'):
251
299
  pass
252
300
 
253
- class FunctionJoinPoint(JoinPoint):
301
+ class FunctionAspect(Aspect):
254
302
  __slots__ = [
255
303
  "instance",
256
304
  "func",
305
+ "order",
257
306
  ]
258
307
 
259
- def __init__(self, instance, func, next: Optional['JoinPoint']):
260
- super().__init__(next)
308
+ def __init__(self, instance, func, next_aspect: Optional['Aspect']):
309
+ super().__init__(next_aspect)
261
310
 
262
311
  self.instance = instance
263
312
  self.func = func
264
313
 
314
+ self.order = next((decorator.args[0] for decorator in Decorators.get(func) if decorator.decorator is order), 0)
315
+
265
316
  def call(self, invocation: 'Invocation'):
266
- invocation.current_join_point = self
317
+ invocation.current_aspect = self
267
318
 
268
319
  return self.func(self.instance, invocation)
269
320
 
270
321
  async def call_async(self, invocation: 'Invocation'):
271
- invocation.current_join_point = self
322
+ invocation.current_aspect = self
272
323
 
273
324
  return await self.func(self.instance, invocation)
274
325
 
275
- class MethodJoinPoint(FunctionJoinPoint):
326
+ class MethodAspect(FunctionAspect):
276
327
  __slots__ = []
277
328
 
278
329
  def __init__(self, instance, func):
279
330
  super().__init__(instance, func, None)
280
331
 
281
332
  def call(self, invocation: 'Invocation'):
282
- invocation.current_join_point = self
333
+ invocation.current_aspect = self
283
334
 
284
335
  return self.func(*invocation.args, **invocation.kwargs)
285
336
 
286
337
  async def call_async(self, invocation: 'Invocation'):
287
- invocation.current_join_point = self
338
+ invocation.current_aspect = self
288
339
 
289
340
  return await self.func(*invocation.args, **invocation.kwargs)
290
341
 
291
342
  @dataclass
292
- class JoinPoints:
293
- before: list[JoinPoint]
294
- around: list[JoinPoint]
295
- error: list[JoinPoint]
296
- after: list[JoinPoint]
343
+ class Aspects:
344
+ before: list[Aspect]
345
+ around: list[Aspect]
346
+ error: list[Aspect]
347
+ after: list[Aspect]
297
348
 
298
349
  class Invocation:
299
350
  """
300
351
  Invocation stores the relevant data of a single method invocation.
301
- It holds the arguments, keyword arguments, result, error, and the join points that define the aspect behavior.
352
+ It holds the arguments, keyword arguments, result, error, and the aspects that define the aspect behavior.
302
353
  """
303
354
  # properties
304
355
 
@@ -308,20 +359,20 @@ class Invocation:
308
359
  "kwargs",
309
360
  "result",
310
361
  "exception",
311
- "join_points",
312
- "current_join_point",
362
+ "aspects",
363
+ "current_aspect",
313
364
  ]
314
365
 
315
366
  # constructor
316
367
 
317
- def __init__(self, func, join_points: JoinPoints):
368
+ def __init__(self, func, aspects: Aspects):
318
369
  self.func = func
319
370
  self.args : list[object] = []
320
371
  self.kwargs = None
321
372
  self.result = None
322
373
  self.exception = None
323
- self.join_points = join_points
324
- self.current_join_point = None
374
+ self.aspects = aspects
375
+ self.current_aspect = None
325
376
 
326
377
  def call(self, *args, **kwargs):
327
378
  # remember args
@@ -331,23 +382,23 @@ class Invocation:
331
382
 
332
383
  # run all before
333
384
 
334
- for join_point in self.join_points.before:
335
- join_point.call(self)
385
+ for aspect in self.aspects.before:
386
+ aspect.call(self)
336
387
 
337
388
  # run around's with the method being the last aspect!
338
389
 
339
390
  try:
340
- self.result = self.join_points.around[0].call(self) # will follow the proceed chain
391
+ self.result = self.aspects.around[0].call(self) # will follow the proceed chain
341
392
 
342
393
  except Exception as e:
343
394
  self.exception = e
344
- for join_point in self.join_points.error:
345
- join_point.call(self)
395
+ for aspect in self.aspects.error:
396
+ aspect.call(self)
346
397
 
347
398
  # run all before
348
399
 
349
- for join_point in self.join_points.after:
350
- join_point.call(self)
400
+ for aspect in self.aspects.after:
401
+ aspect.call(self)
351
402
 
352
403
  if self.exception is not None:
353
404
  raise self.exception # rethrow the error
@@ -362,23 +413,23 @@ class Invocation:
362
413
 
363
414
  # run all before
364
415
 
365
- for join_point in self.join_points.before:
366
- join_point.call(self)
416
+ for aspect in self.aspects.before:
417
+ aspect.call(self)
367
418
 
368
419
  # run around's with the method being the last aspect!
369
420
 
370
421
  try:
371
- self.result = await self.join_points.around[0].call_async(self) # will follow the proceed chain
422
+ self.result = await self.aspects.around[0].call_async(self) # will follow the proceed chain
372
423
 
373
424
  except Exception as e:
374
425
  self.exception = e
375
- for join_point in self.join_points.error:
376
- join_point.call(self)
426
+ for aspect in self.aspects.error:
427
+ aspect.call(self)
377
428
 
378
429
  # run all before
379
430
 
380
- for join_point in self.join_points.after:
381
- join_point.call(self)
431
+ for aspect in self.aspects.after:
432
+ aspect.call(self)
382
433
 
383
434
  if self.exception is not None:
384
435
  raise self.exception # rethrow the error
@@ -387,7 +438,7 @@ class Invocation:
387
438
 
388
439
  def proceed(self, *args, **kwargs):
389
440
  """
390
- Proceed to the next join point in the around chain up to the original method.
441
+ Proceed to the next aspect in the around chain up to the original method.
391
442
  """
392
443
  if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
393
444
  self.args = args
@@ -395,11 +446,11 @@ class Invocation:
395
446
 
396
447
  # next one please...
397
448
 
398
- return self.current_join_point.next.call(self)
449
+ return self.current_aspect.next.call(self)
399
450
 
400
451
  async def proceed_async(self, *args, **kwargs):
401
452
  """
402
- Proceed to the next join point in the around chain up to the original method.
453
+ Proceed to the next aspect in the around chain up to the original method.
403
454
  """
404
455
  if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
405
456
  self.args = args
@@ -407,29 +458,38 @@ class Invocation:
407
458
 
408
459
  # next one please...
409
460
 
410
- return await self.current_join_point.next.call_async(self)
461
+ return await self.current_aspect.next.call_async(self)
411
462
 
412
- @injectable()
413
- class Advice:
463
+ class Advices:
464
+ """
465
+ Internal utility class that collects all advice s
466
+ """
414
467
  # static data
415
468
 
416
469
  targets: list[AspectTarget] = []
417
470
 
418
- __slots__ = [
419
- "cache",
420
- "lock"
421
- ]
471
+ __slots__ = []
422
472
 
423
473
  # constructor
424
474
 
425
475
  def __init__(self):
426
- self.cache : Dict[Type, Dict[Callable,JoinPoints]] = {}
427
- self.lock = threading.RLock()
476
+ pass
428
477
 
429
478
  # methods
430
479
 
431
- def collect(self, clazz, member, type: AspectType, environment: Environment):
432
- aspects = [FunctionJoinPoint(environment.get(target._clazz), target._function, None) for target in Advice.targets if target._type == type and target._matches(clazz, member)]
480
+ @classmethod
481
+ def collect(cls, clazz, member, type: AspectType, environment: Environment):
482
+ aspects = [
483
+ FunctionAspect(environment.get(target._clazz), target._function, None) for target in Advices.targets
484
+ if target._type == type
485
+ and target._clazz is not clazz
486
+ and environment.providers.get(target._clazz) is not None
487
+ and target._matches(clazz, member)
488
+ ]
489
+
490
+ # sort according to order
491
+
492
+ aspects = sorted(aspects, key=lambda aspect: aspect.order)
433
493
 
434
494
  # link
435
495
 
@@ -440,31 +500,23 @@ class Advice:
440
500
 
441
501
  return aspects
442
502
 
443
- def join_points4(self, instance, environment: Environment) -> Dict[Callable,JoinPoints]:
503
+ @classmethod
504
+ def aspects_for(cls, instance, environment: Environment) -> Dict[Callable,Aspects]:
444
505
  clazz = type(instance)
445
506
 
446
- result = self.cache.get(clazz, None)
447
- if result is None:
448
- with self.lock:
449
- result = self.cache.get(clazz, None)
450
-
451
- if result is None:
452
- result = {}
453
- self.cache[clazz] = result
454
-
455
- for _, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
456
- join_points = self.compute_join_points(clazz, member, environment)
457
- if join_points is not None:
458
- result[member] = join_points
459
-
507
+ result = {}
460
508
 
509
+ for _, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
510
+ aspects = cls.compute_aspects(clazz, member, environment)
511
+ if aspects is not None:
512
+ result[member] = aspects
461
513
 
462
514
  # add around methods
463
515
 
464
516
  value = {}
465
517
 
466
518
  for key, cjp in result.items():
467
- jp = JoinPoints(
519
+ jp = Aspects(
468
520
  before=cjp.before,
469
521
  around=cjp.around,
470
522
  error=cjp.error,
@@ -472,7 +524,7 @@ class Advice:
472
524
 
473
525
  # add method to around
474
526
 
475
- jp.around.append(MethodJoinPoint(instance, key))
527
+ jp.around.append(MethodAspect(instance, key))
476
528
  if len(jp.around) > 1:
477
529
  jp.around[len(jp.around) - 2].next = jp.around[len(jp.around) - 1]
478
530
 
@@ -482,14 +534,15 @@ class Advice:
482
534
 
483
535
  return value
484
536
 
485
- def compute_join_points(self, clazz, member, environment: Environment) -> Optional[JoinPoints]:
486
- befores = self.collect(clazz, member, AspectType.BEFORE, environment)
487
- arounds = self.collect(clazz, member, AspectType.AROUND, environment)
488
- afters = self.collect(clazz, member, AspectType.AFTER, environment)
489
- errors = self.collect(clazz, member, AspectType.ERROR, environment)
537
+ @classmethod
538
+ def compute_aspects(cls, clazz, member, environment: Environment) -> Optional[Aspects]:
539
+ befores = cls.collect(clazz, member, AspectType.BEFORE, environment)
540
+ arounds = cls.collect(clazz, member, AspectType.AROUND, environment)
541
+ afters = cls.collect(clazz, member, AspectType.AFTER, environment)
542
+ errors = cls.collect(clazz, member, AspectType.ERROR, environment)
490
543
 
491
544
  if len(befores) > 0 or len(arounds) > 0 or len(afters) > 0 or len(errors) > 0:
492
- return JoinPoints(
545
+ return Aspects(
493
546
  before=befores,
494
547
  around=arounds,
495
548
  error=errors,
@@ -507,8 +560,8 @@ def sanity_check(clazz: Type, name: str):
507
560
 
508
561
  def advice(cls):
509
562
  """
510
- Classes decorated with @advice are treated as aspect classes.
511
- They can contain methods decorated with @before, @after, @around, or @error to define aspects.
563
+ Classes decorated with `@advice` are treated as advice classes.
564
+ They can contain methods decorated with `@before`, `@after`, `@around`, or `@error` to define aspects.
512
565
  """
513
566
  Providers.register(ClassInstanceProvider(cls, True))
514
567
 
@@ -517,10 +570,10 @@ def advice(cls):
517
570
  for name, member in TypeDescriptor.for_type(cls).methods.items():
518
571
  decorator = next((decorator for decorator in member.decorators if decorator.decorator in [before, after, around, error]), None)
519
572
  if decorator is not None:
520
- target = decorator.args[0]
573
+ target = decorator.args[0] # multiple targets are already merged in a single! check _register
521
574
  target._clazz = cls
522
575
  sanity_check(cls, name)
523
- Advice.targets.append(target)
576
+ Advices.targets.append(target) #??
524
577
 
525
578
  return cls
526
579
 
@@ -539,7 +592,7 @@ def _register(decorator, targets: list[AspectTarget], func, aspect_type: AspectT
539
592
 
540
593
  def before(*targets: AspectTarget):
541
594
  """
542
- Methods decorated with @before will be executed before the target method is invoked.
595
+ Methods decorated with `@before` will be executed before the target method is invoked.
543
596
  """
544
597
  def decorator(func):
545
598
  _register(before, targets, func, AspectType.BEFORE)
@@ -550,7 +603,7 @@ def before(*targets: AspectTarget):
550
603
 
551
604
  def error(*targets: AspectTarget):
552
605
  """
553
- Methods decorated with @error will be executed if the target method raises an exception."""
606
+ Methods decorated with `@error` will be executed if the target method raises an exception."""
554
607
  def decorator(func):
555
608
  _register(error, targets, func, AspectType.ERROR)
556
609
 
@@ -560,7 +613,7 @@ def error(*targets: AspectTarget):
560
613
 
561
614
  def after(*targets: AspectTarget):
562
615
  """
563
- Methods decorated with @after will be executed after the target method is invoked.
616
+ Methods decorated with `@after` will be executed after the target method is invoked.
564
617
  """
565
618
  def decorator(func):
566
619
  _register(after, targets, func, AspectType.AFTER)
@@ -571,7 +624,7 @@ def after(*targets: AspectTarget):
571
624
 
572
625
  def around(*targets: AspectTarget):
573
626
  """
574
- Methods decorated with @around will be executed around the target method.
627
+ Methods decorated with `@around` will be executed around the target method.
575
628
  Every around method must accept a single parameter of type Invocation and needs to call proceed
576
629
  on this parameter to proceed to the next around method.
577
630
  """
@@ -582,28 +635,42 @@ def around(*targets: AspectTarget):
582
635
 
583
636
  return decorator
584
637
 
585
- @injectable()
638
+ @injectable(scope="environment")
586
639
  @order(0)
587
640
  class AdviceProcessor(PostProcessor):
588
641
  # properties
589
642
 
590
643
  __slots__ = [
591
- "advice",
644
+ "lock",
645
+ "cache"
592
646
  ]
593
647
 
594
648
  # constructor
595
649
 
596
- def __init__(self, advice: Advice):
650
+ def __init__(self):
597
651
  super().__init__()
598
652
 
599
- self.advice = advice
653
+ self.cache : Dict[Type, Dict[Callable,Aspects]] = {}
654
+ self.lock = threading.RLock()
655
+
656
+ # local
657
+
658
+ def aspects_for(self, instance, environment: Environment) -> Dict[Callable,Aspects]:
659
+ clazz = type(instance)
660
+ result = self.cache.get(clazz, None)
661
+ if result is None:
662
+ with self.lock:
663
+ result = Advices.aspects_for(instance, environment)
664
+ self.cache[clazz] = result # TOID der cache ist zu dick?????
665
+
666
+ return result
600
667
 
601
668
  # implement
602
669
 
603
670
  def process(self, instance: object, environment: Environment):
604
- join_point_dict = self.advice.join_points4(instance, environment)
671
+ aspect_dict = self.aspects_for(instance, environment)
605
672
 
606
- for member, join_points in join_point_dict.items():
673
+ for member, aspects in aspect_dict.items():
607
674
  Environment.logger.debug("add aspects for %s:%s", type(instance), member.__name__)
608
675
 
609
676
  def wrap(jp):
@@ -619,6 +686,6 @@ class AdviceProcessor(PostProcessor):
619
686
  return async_wrapper
620
687
 
621
688
  if inspect.iscoroutinefunction(member):
622
- setattr(instance, member.__name__, types.MethodType(wrap_async(join_points), instance))
689
+ setattr(instance, member.__name__, types.MethodType(wrap_async(aspects), instance))
623
690
  else:
624
- setattr(instance, member.__name__, types.MethodType(wrap(join_points), instance))
691
+ setattr(instance, member.__name__, types.MethodType(wrap(aspects), instance))
@@ -1,7 +1,7 @@
1
1
  """
2
- Configuration value handling
2
+ This module contains functionality to read configuration values from different sources and to retrieve or inject them.
3
3
  """
4
- from .configuration import ConfigurationManager, ConfigurationSource, value
4
+ from .configuration import ConfigurationManager, ConfigurationSource, inject_value
5
5
  from .env_configuration_source import EnvConfigurationSource
6
6
  from .yaml_configuration_source import YamlConfigurationSource
7
7
 
@@ -10,5 +10,5 @@ __all__ = [
10
10
  "ConfigurationSource",
11
11
  "EnvConfigurationSource",
12
12
  "YamlConfigurationSource",
13
- "value"
13
+ "inject_value"
14
14
  ]