aspyx 0.1.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/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ from .di import InjectorException, CallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, create, on_init, on_destroy, inject_environment, Factory, PostProcessor
2
+
3
+ # import something from the subpackages, so that teh decorators are executed
4
+
5
+ from aspyx.di.configuration import ConfigurationManager
6
+ from aspyx.di.aop import before
7
+
8
+ imports = [ConfigurationManager, before]
9
+
10
+ __all__ = [
11
+ "ClassInstanceProvider",
12
+ "Providers",
13
+ "Environment",
14
+ "injectable",
15
+ "factory",
16
+ "environment",
17
+ "inject",
18
+ "create",
19
+
20
+ "on_init",
21
+ "on_destroy",
22
+ "inject_environment",
23
+ "Factory",
24
+ "PostProcessor",
25
+ "CallableProcessor",
26
+ "LifecycleCallable",
27
+ "InjectorException",
28
+ "Lifecycle"
29
+ ]
@@ -0,0 +1,11 @@
1
+ from .aop import before, after, classes, around, error, advice, methods, Invocation
2
+ __all__ = [
3
+ "before",
4
+ "after",
5
+ "around",
6
+ "error",
7
+ "advice",
8
+ "classes",
9
+ "methods",
10
+ "Invocation",
11
+ ]
aspyx/di/aop/aop.py ADDED
@@ -0,0 +1,532 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ import inspect
5
+ import re
6
+ import types
7
+ from dataclasses import dataclass
8
+ from enum import auto, Enum
9
+ from typing import Optional, Dict, Type, Callable
10
+
11
+ from aspyx.reflection import Decorators, TypeDescriptor
12
+ from aspyx.di import injectable, Providers, ClassInstanceProvider, Environment, PostProcessor
13
+
14
+
15
+ class AOPException(Exception):
16
+ """
17
+ Exception raised for errors in the aop logic."""
18
+ pass
19
+
20
+ class AspectType(Enum):
21
+ """
22
+ AspectType defines the types of aspect-oriented advice that can be applied to methods.
23
+
24
+ The available types are:
25
+ - BEFORE: Advice to be executed before the method invocation.
26
+ - AROUND: Advice that intercepts the method invocation.
27
+ - AFTER: Advice to be executed after the method invocation, regardless of its outcome.
28
+ - ERROR: Advice to be executed if the method invocation raises an exception.
29
+
30
+ These types are used to categorize and apply aspect logic at different points in a method's execution lifecycle.
31
+ """
32
+ BEFORE = auto()
33
+ AROUND = auto()
34
+ AFTER = auto()
35
+ ERROR = auto()
36
+
37
+ class AspectTarget(ABC):
38
+ """
39
+ AspectTarget defines the target for an aspect. It can be used to specify the class, method, and conditions under which the aspect should be applied.
40
+ It supports matching by class type, method name, patterns, decorators, and more.
41
+ """
42
+
43
+ # properties
44
+
45
+ __slots__ = [
46
+ "_function",
47
+ "_type",
48
+
49
+ "_clazz",
50
+ "_instance",
51
+ "names",
52
+ "patterns",
53
+ "types",
54
+ "decorators",
55
+ ]
56
+
57
+ # constructor
58
+
59
+ def __init__(self):
60
+ self._clazz = None
61
+ self._instance = None
62
+
63
+ self.patterns = []
64
+ self.names = []
65
+ self.types = []
66
+ self.decorators = []
67
+
68
+ pass
69
+
70
+ # abstract
71
+
72
+ @abstractmethod
73
+ def _matches(self, clazz : Type, func):
74
+ pass
75
+
76
+ # fluent
77
+
78
+ def function(self, func):
79
+ self._function = func
80
+ return self
81
+
82
+ def type(self, type: AspectType):
83
+ self._type = type
84
+
85
+ return self
86
+
87
+ def of_type(self, type: Type):
88
+ self.types.append(type)
89
+ return self
90
+
91
+ def decorated_with(self, decorator):
92
+ self.decorators.append(decorator)
93
+ return self
94
+
95
+ def matches(self, pattern: str):
96
+ """
97
+ Matches the target against a pattern.
98
+ """
99
+ self.patterns.append(re.compile(pattern))
100
+ return self
101
+
102
+ def named(self, name: str):
103
+ self.names.append(name)
104
+ return self
105
+
106
+ class ClassAspectTarget(AspectTarget):
107
+ # properties
108
+
109
+ __slots__ = [
110
+ ]
111
+
112
+ # constructor
113
+
114
+ def __init__(self):
115
+ super().__init__()
116
+
117
+ pass
118
+
119
+ # public
120
+
121
+ def _matches(self, clazz : Type, func):
122
+ descriptor = TypeDescriptor.for_type(clazz)
123
+
124
+ # type
125
+
126
+ if len(self.types) > 0:
127
+ if next((type for type in self.types if issubclass(clazz, type)), None) is None:
128
+ return False
129
+
130
+ # decorators
131
+
132
+ if len(self.decorators) > 0:
133
+ if next((decorator for decorator in self.decorators if descriptor.has_decorator(decorator)), None) is None:
134
+ return False
135
+
136
+ # names
137
+
138
+ if len(self.names) > 0:
139
+ if next((name for name in self.names if name == clazz.__name__), None) is None:
140
+ return False
141
+
142
+ # patterns
143
+
144
+ if len(self.patterns) > 0:
145
+ if next((pattern for pattern in self.patterns if re.fullmatch(pattern, clazz.__name__) is not None), None) is None:
146
+ return False
147
+
148
+ # yipee
149
+
150
+ return True
151
+
152
+ # fluent
153
+
154
+
155
+
156
+ class MethodAspectTarget(AspectTarget):
157
+ # properties
158
+
159
+ __slots__ = [
160
+ "_clazz",
161
+ "_instance",
162
+ "_type",
163
+ "_function",
164
+ "names",
165
+ "patterns",
166
+ "types",
167
+ "decorators",
168
+ ]
169
+
170
+ # constructor
171
+
172
+ def __init__(self):
173
+ super().__init__()
174
+
175
+ # public
176
+
177
+ def _matches(self, clazz : Type, func):
178
+ descriptor = TypeDescriptor.for_type(clazz)
179
+
180
+ methodDescriptor = descriptor.get_method(func.__name__)
181
+
182
+ # type
183
+
184
+ if len(self.types) > 0:
185
+ if next((type for type in self.types if issubclass(clazz, type)), None) is None:
186
+ return False
187
+
188
+ # decorators
189
+
190
+ if len(self.decorators) > 0:
191
+ if next((decorator for decorator in self.decorators if methodDescriptor.has_decorator(decorator)), None) is None:
192
+ return False
193
+
194
+ # names
195
+
196
+ if len(self.names) > 0:
197
+ if next((name for name in self.names if name == func.__name__), None) is None:
198
+ return False
199
+
200
+ # patterns
201
+
202
+ if len(self.patterns) > 0:
203
+ if next((pattern for pattern in self.patterns if re.fullmatch(pattern, func.__name__) is not None), None) is None:
204
+ return False
205
+
206
+ # yipee
207
+
208
+ return True
209
+
210
+ def methods():
211
+ """
212
+ Create a new AspectTarget instance to define method aspect targets.
213
+ """
214
+ return MethodAspectTarget()
215
+
216
+ def classes():
217
+ """
218
+ Create a new AspectTarget instance to define class aspect targets.
219
+ """
220
+ return ClassAspectTarget()
221
+
222
+
223
+ class JoinPoint:
224
+ __slots__ = [
225
+ "next",
226
+ ]
227
+
228
+ # constructor
229
+
230
+ def __init__(self, next: 'JoinPoint'):
231
+ self.next = next
232
+
233
+ # public
234
+
235
+ def call(self, invocation: 'Invocation'):
236
+ pass
237
+
238
+ class FunctionJoinPoint(JoinPoint):
239
+ __slots__ = [
240
+ "instance",
241
+ "func",
242
+ ]
243
+
244
+ def __init__(self, instance, func, next: Optional['JoinPoint']):
245
+ super().__init__(next)
246
+
247
+ self.instance = instance
248
+ self.func = func
249
+
250
+ def call(self, invocation: 'Invocation'):
251
+ invocation.currentJoinPoint = self
252
+
253
+ return self.func(self.instance, invocation)
254
+
255
+ class MethodJoinPoint(FunctionJoinPoint):
256
+ __slots__ = []
257
+
258
+ def __init__(self, instance, func):
259
+ super().__init__(instance, func, None)
260
+
261
+ def call(self, invocation: 'Invocation'):
262
+ invocation.currentJoinPoint = self
263
+
264
+ return self.func(*invocation.args, **invocation.kwargs)
265
+
266
+ @dataclass
267
+ class JoinPoints:
268
+ before: list[JoinPoint]
269
+ around: list[JoinPoint]
270
+ error: list[JoinPoint]
271
+ after: list[JoinPoint]
272
+
273
+ class Invocation:
274
+ """
275
+ Invocation stores the relevant data of a single method invocation.
276
+ It holds the arguments, keyword arguments, result, error, and the join points that define the aspect behavior.
277
+ """
278
+ # properties
279
+
280
+ __slots__ = [
281
+ "func",
282
+ "args",
283
+ "kwargs",
284
+ "result",
285
+ "exception",
286
+ "joinPoints",
287
+ "currentJoinPoint",
288
+ ]
289
+
290
+ # constructor
291
+
292
+ def __init__(self, func, joinPoints: JoinPoints):
293
+ self.func = func
294
+ self.args : list[object] = []
295
+ self.kwargs = None
296
+ self.result = None
297
+ self.exception = None
298
+ self.joinPoints = joinPoints
299
+ self.currentJoinPoint = None
300
+
301
+ def call(self, *args, **kwargs):
302
+ # remember args
303
+
304
+ self.args = args
305
+ self.kwargs = kwargs
306
+
307
+ # run all before
308
+
309
+ for joinPoint in self.joinPoints.before:
310
+ joinPoint.call(self)
311
+
312
+ # run around's with the method being the last aspect!
313
+
314
+ try:
315
+ self.result = self.joinPoints.around[0].call(self) # will follow the proceed chain
316
+
317
+ except Exception as e:
318
+ self.exception = e
319
+ for joinPoint in self.joinPoints.error:
320
+ joinPoint.call(self)
321
+
322
+ # run all before
323
+
324
+ for joinPoint in self.joinPoints.after:
325
+ joinPoint.call(self)
326
+
327
+ if self.exception is not None:
328
+ raise self.exception # rethrow the error
329
+ else:
330
+ return self.result
331
+
332
+ def proceed(self, *args, **kwargs):
333
+ """
334
+ Proceed to the next join point in the around chain up to the original method.
335
+ """
336
+ if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
337
+ self.args = args
338
+ self.kwargs = kwargs
339
+
340
+ # next one please...
341
+
342
+ return self.currentJoinPoint.next.call(self)
343
+
344
+ @injectable()
345
+ class Advice:
346
+ # static data
347
+
348
+ targets: list[AspectTarget] = []
349
+
350
+ __slots__ = [
351
+ "cache",
352
+ ]
353
+
354
+ # constructor
355
+
356
+ def __init__(self):
357
+ self.cache : Dict[Type, Dict[Callable,JoinPoints]] = dict()
358
+
359
+ # methods
360
+
361
+ def collect(self, clazz, member, type: AspectType, environment: Environment):
362
+ aspects = [FunctionJoinPoint(environment.get(target._clazz), target._function, None) for target in Advice.targets if target._type == type and target._matches(clazz, member)]
363
+
364
+ # link
365
+
366
+ for i in range(0, len(aspects) - 1):
367
+ aspects[i].next = aspects[i + 1]
368
+
369
+ # done
370
+
371
+ return aspects
372
+
373
+ # TODO thread-safe
374
+ def joinPoints4(self, instance, environment: Environment) -> Dict[Callable,JoinPoints]:
375
+ clazz = type(instance)
376
+
377
+ result = self.cache.get(clazz, None)
378
+ if result is None:
379
+ result = dict()
380
+
381
+ for name, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
382
+ joinPoints = self.computeJoinPoints(clazz, member, environment)
383
+ if joinPoints is not None:
384
+ result[member] = joinPoints
385
+
386
+ self.cache[clazz] = result
387
+
388
+ # add around methods
389
+
390
+ value = dict()
391
+
392
+ for key, cjp in result.items():
393
+ jp = JoinPoints(
394
+ before=cjp.before,
395
+ around=cjp.around,
396
+ error=cjp.error,
397
+ after=cjp.after)
398
+
399
+ # add method to around
400
+
401
+ jp.around.append(MethodJoinPoint(instance, key))
402
+ if len(jp.around) > 1:
403
+ jp.around[len(jp.around) - 2].next = jp.around[len(jp.around) - 1]
404
+
405
+ value[key] = jp
406
+
407
+ # done
408
+
409
+ return value
410
+
411
+ def computeJoinPoints(self, clazz, member, environment: Environment) -> Optional[JoinPoints]:
412
+ befores = self.collect(clazz, member, AspectType.BEFORE, environment)
413
+ arounds = self.collect(clazz, member, AspectType.AROUND, environment)
414
+ afters = self.collect(clazz, member, AspectType.AFTER, environment)
415
+ errors = self.collect(clazz, member, AspectType.ERROR, environment)
416
+
417
+ if len(befores) > 0 or len(arounds) > 0 or len(afters) > 0 or len(errors) > 0:
418
+ return JoinPoints(
419
+ before=befores,
420
+ around=arounds,
421
+ error=errors,
422
+ after=afters
423
+ )
424
+ else:
425
+ return None
426
+
427
+ def sanityCheck(clazz: Type, name: str):
428
+ m = TypeDescriptor.for_type(clazz).get_method(name)
429
+ if len(m.paramTypes) != 1 or m.paramTypes[0] != Invocation:
430
+ raise AOPException(f"Method {clazz.__name__}.{name} expected to have one parameter of type Invocation")
431
+
432
+ # decorators
433
+
434
+ def advice(cls):
435
+ """
436
+ Classes decorated with @advice are treated as aspect classes.
437
+ They can contain methods decorated with @before, @after, @around, or @error to define aspects.
438
+ """
439
+ Providers.register(ClassInstanceProvider(cls, True))
440
+
441
+ Decorators.add(cls, advice)
442
+
443
+ for name, member in TypeDescriptor.for_type(cls).methods.items():
444
+ decorator = next((decorator for decorator in member.decorators if decorator.decorator in [before, after, around, error]), None)
445
+ if decorator is not None:
446
+ target = decorator.args[0]
447
+ target._clazz = cls
448
+ sanityCheck(cls, name)
449
+ Advice.targets.append(target)
450
+
451
+ return cls
452
+
453
+
454
+ # decorators
455
+
456
+ def _register(decorator, target: AspectTarget, func, aspectType: AspectType):
457
+ target.function(func).type(aspectType)
458
+
459
+ Decorators.add(func, decorator, target)
460
+
461
+ def before(target: AspectTarget):
462
+ """
463
+ Methods decorated with @before will be executed before the target method is invoked.
464
+ """
465
+ def decorator(func):
466
+ _register(before, target, func, AspectType.BEFORE)
467
+
468
+ return func
469
+
470
+ return decorator
471
+
472
+ def error(target: AspectTarget):
473
+ """
474
+ Methods decorated with @error will be executed if the target method raises an exception."""
475
+ def decorator(func):
476
+ _register(error, target, func, AspectType.ERROR)
477
+
478
+ return func
479
+
480
+ return decorator
481
+
482
+ def after(target: AspectTarget):
483
+ """
484
+ Methods decorated with @after will be executed after the target method is invoked.
485
+ """
486
+ def decorator(func):
487
+ _register(after, target, func, AspectType.AFTER)
488
+
489
+ return func
490
+
491
+ return decorator
492
+
493
+ def around(target: AspectTarget):
494
+ """
495
+ Methods decorated with @around will be executed around the target method.
496
+ Every around method must accept a single parameter of type Invocation and needs to call proceed
497
+ on this parameter to proceed to the next around method.
498
+ """
499
+ def decorator(func):
500
+ _register(around, target, func, AspectType.AROUND)
501
+
502
+ return func
503
+
504
+ return decorator
505
+
506
+ @injectable()
507
+ class AdviceProcessor(PostProcessor):
508
+ # properties
509
+
510
+ __slots__ = [
511
+ "advice",
512
+ ]
513
+
514
+ # constructor
515
+
516
+ def __init__(self, advice: Advice):
517
+ super().__init__()
518
+
519
+ self.advice = advice
520
+
521
+ # implement
522
+
523
+ def process(self, instance: object, environment: Environment):
524
+ joinPointDict = self.advice.joinPoints4(instance, environment)
525
+
526
+ for member, joinPoints in joinPointDict.items():
527
+ Environment.logger.debug(f"add aspects for {type(instance)}:{member.__name__}")
528
+
529
+ def wrap(jp):
530
+ return lambda *args, **kwargs: Invocation(member, jp).call(*args, **kwargs)
531
+
532
+ setattr(instance, member.__name__, types.MethodType(wrap(joinPoints), instance))
@@ -0,0 +1,8 @@
1
+ from .configuration import ConfigurationManager, ConfigurationSource, EnvConfigurationSource, value
2
+
3
+ __all__ = [
4
+ "ConfigurationManager",
5
+ "ConfigurationSource",
6
+ "EnvConfigurationSource",
7
+ "value"
8
+ ]