aspyx 0.1.0__tar.gz → 1.0.0__tar.gz

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.
Files changed (24) hide show
  1. {aspyx-0.1.0/src/aspyx.egg-info → aspyx-1.0.0}/PKG-INFO +54 -11
  2. {aspyx-0.1.0 → aspyx-1.0.0}/README.md +53 -10
  3. {aspyx-0.1.0 → aspyx-1.0.0}/pyproject.toml +1 -1
  4. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/di/aop/aop.py +55 -25
  5. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/di/configuration/configuration.py +2 -0
  6. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/di/di.py +49 -6
  7. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/reflection/reflection.py +8 -3
  8. {aspyx-0.1.0 → aspyx-1.0.0/src/aspyx.egg-info}/PKG-INFO +54 -11
  9. {aspyx-0.1.0 → aspyx-1.0.0}/tests/test_aop.py +5 -4
  10. {aspyx-0.1.0 → aspyx-1.0.0}/tests/test_di.py +3 -1
  11. {aspyx-0.1.0 → aspyx-1.0.0}/tests/test_reflection.py +19 -2
  12. {aspyx-0.1.0 → aspyx-1.0.0}/LICENSE +0 -0
  13. {aspyx-0.1.0 → aspyx-1.0.0}/setup.cfg +0 -0
  14. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/di/__init__.py +0 -0
  15. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/di/aop/__init__.py +0 -0
  16. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/di/configuration/__init__.py +0 -0
  17. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/reflection/__init__.py +0 -0
  18. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx/reflection/proxy.py +0 -0
  19. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx.egg-info/SOURCES.txt +0 -0
  20. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
  21. {aspyx-0.1.0 → aspyx-1.0.0}/src/aspyx.egg-info/top_level.txt +0 -0
  22. {aspyx-0.1.0 → aspyx-1.0.0}/tests/test_configuration.py +0 -0
  23. {aspyx-0.1.0 → aspyx-1.0.0}/tests/test_di_cycle.py +0 -0
  24. {aspyx-0.1.0 → aspyx-1.0.0}/tests/test_proxy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 0.1.0
3
+ Version: 1.0.0
4
4
  Summary: A DI and AOP library for Python
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -32,8 +32,8 @@ Dynamic: license-file
32
32
 
33
33
  # aspyx
34
34
 
35
- ![Pylint](https://github.com/andreasernst/aspyx/actions/workflows/pylint.yml/badge.svg)
36
- ![Build Status](https://github.com/andreasernst/aspyx/actions/workflows/ci.yml/badge.svg)
35
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
36
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
37
37
 
38
38
 
39
39
  ## Table of Contents
@@ -46,8 +46,10 @@ Dynamic: license-file
46
46
  - [Environment](#environment)
47
47
  - [Definition](#definition)
48
48
  - [Retrieval](#retrieval)
49
- - [Lifecycle methods](#lifecycle-methods)
50
- - [Post Processors](#post-processors)
49
+ - [Instantiation logic](#instantiation-logic)
50
+ - [Injection Methods](#injection-methods)
51
+ - [Lifecycle Methods](#lifecycle-methods)
52
+ - [Post Processors](#post-processors)
51
53
  - [Custom scopes](#custom-scopes)
52
54
  - [AOP](#aop)
53
55
  - [Configuration](#configuration)
@@ -164,9 +166,10 @@ class Foo:
164
166
  def __init__(self):
165
167
  pass
166
168
  ```
167
- Please make sure, that the class defines a constructor, as this is required to determine injected instances.
169
+ Please make sure, that the class defines a local constructor, as this is required to determine injected instances.
170
+ All referenced types will be injected by the environemnt.
168
171
 
169
- The constructor can only define parameter types that are known as well to the container!
172
+ Only eligible types are allowed, of course!
170
173
 
171
174
 
172
175
  The decorator accepts the keyword arguments
@@ -268,15 +271,55 @@ In case of ambiguities, it will throw an exception.
268
271
 
269
272
  Please be aware, that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
270
273
 
271
- # Lifecycle methods
274
+ # Instantiation logic
272
275
 
273
- It is possible to declare methods that will be called from the container
276
+ Constructing a new instance involves a number of steps executed in this order
277
+ - Constructor call
278
+ the constructor is called with the resolved parameters
279
+ - Advice injection
280
+ All methods involving aspects are updated
281
+ - Lifecycle methods
282
+ different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
283
+ These are various injection possibilities as well as an optional final `on_init` call
284
+ - PostProcessors
285
+ Any custom post processors, that can add isde effects or modify the instances
286
+
287
+ ## Injection methods
288
+
289
+ Different decorators are implemented, that call methods with computed values
290
+
291
+ - `@inject`
292
+ the method is called with all resolved parameter types ( same as the constructor call)
293
+ - `@inject_environment`
294
+ the method is called with the creating environment as a single parameter
295
+ - `@value()`
296
+ the method is called with a resolved configuration value. Check the corresponding chapter
297
+
298
+ **Example**:
299
+ ```python
300
+ @injectable()
301
+ class Foo:
302
+ def __init__(self):
303
+ pass
304
+
305
+ @inject_environment()
306
+ def initEnvironment(self, env: Environment):
307
+ ...
308
+
309
+ @inject()
310
+ def set(self, baz: Baz) -> None:
311
+ ...
312
+ ```
313
+
314
+ ## Lifecycle methods
315
+
316
+ It is possible to mark specific lifecle methods.
274
317
  - `@on_init()`
275
318
  called after the constructor and all other injections.
276
319
  - `@on_destroy()`
277
- called after the container has been shut down
320
+ called during shutdown of the environment
278
321
 
279
- # Post Processors
322
+ ## Post Processors
280
323
 
281
324
  As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
282
325
 
@@ -1,7 +1,7 @@
1
1
  # aspyx
2
2
 
3
- ![Pylint](https://github.com/andreasernst/aspyx/actions/workflows/pylint.yml/badge.svg)
4
- ![Build Status](https://github.com/andreasernst/aspyx/actions/workflows/ci.yml/badge.svg)
3
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
4
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
5
5
 
6
6
 
7
7
  ## Table of Contents
@@ -14,8 +14,10 @@
14
14
  - [Environment](#environment)
15
15
  - [Definition](#definition)
16
16
  - [Retrieval](#retrieval)
17
- - [Lifecycle methods](#lifecycle-methods)
18
- - [Post Processors](#post-processors)
17
+ - [Instantiation logic](#instantiation-logic)
18
+ - [Injection Methods](#injection-methods)
19
+ - [Lifecycle Methods](#lifecycle-methods)
20
+ - [Post Processors](#post-processors)
19
21
  - [Custom scopes](#custom-scopes)
20
22
  - [AOP](#aop)
21
23
  - [Configuration](#configuration)
@@ -132,9 +134,10 @@ class Foo:
132
134
  def __init__(self):
133
135
  pass
134
136
  ```
135
- Please make sure, that the class defines a constructor, as this is required to determine injected instances.
137
+ Please make sure, that the class defines a local constructor, as this is required to determine injected instances.
138
+ All referenced types will be injected by the environemnt.
136
139
 
137
- The constructor can only define parameter types that are known as well to the container!
140
+ Only eligible types are allowed, of course!
138
141
 
139
142
 
140
143
  The decorator accepts the keyword arguments
@@ -236,15 +239,55 @@ In case of ambiguities, it will throw an exception.
236
239
 
237
240
  Please be aware, that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
238
241
 
239
- # Lifecycle methods
242
+ # Instantiation logic
240
243
 
241
- It is possible to declare methods that will be called from the container
244
+ Constructing a new instance involves a number of steps executed in this order
245
+ - Constructor call
246
+ the constructor is called with the resolved parameters
247
+ - Advice injection
248
+ All methods involving aspects are updated
249
+ - Lifecycle methods
250
+ different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
251
+ These are various injection possibilities as well as an optional final `on_init` call
252
+ - PostProcessors
253
+ Any custom post processors, that can add isde effects or modify the instances
254
+
255
+ ## Injection methods
256
+
257
+ Different decorators are implemented, that call methods with computed values
258
+
259
+ - `@inject`
260
+ the method is called with all resolved parameter types ( same as the constructor call)
261
+ - `@inject_environment`
262
+ the method is called with the creating environment as a single parameter
263
+ - `@value()`
264
+ the method is called with a resolved configuration value. Check the corresponding chapter
265
+
266
+ **Example**:
267
+ ```python
268
+ @injectable()
269
+ class Foo:
270
+ def __init__(self):
271
+ pass
272
+
273
+ @inject_environment()
274
+ def initEnvironment(self, env: Environment):
275
+ ...
276
+
277
+ @inject()
278
+ def set(self, baz: Baz) -> None:
279
+ ...
280
+ ```
281
+
282
+ ## Lifecycle methods
283
+
284
+ It is possible to mark specific lifecle methods.
242
285
  - `@on_init()`
243
286
  called after the constructor and all other injections.
244
287
  - `@on_destroy()`
245
- called after the container has been shut down
288
+ called during shutdown of the environment
246
289
 
247
- # Post Processors
290
+ ## Post Processors
248
291
 
249
292
  As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
250
293
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aspyx"
7
- version = "0.1.0"
7
+ version = "1.0.0"
8
8
  description = "A DI and AOP library for Python"
9
9
  authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
10
10
  readme = "README.md"
@@ -3,11 +3,13 @@ from __future__ import annotations
3
3
  from abc import ABC, abstractmethod
4
4
  import inspect
5
5
  import re
6
+ import threading
6
7
  import types
7
8
  from dataclasses import dataclass
8
9
  from enum import auto, Enum
9
10
  from typing import Optional, Dict, Type, Callable
10
11
 
12
+ from aspyx.di.di import order
11
13
  from aspyx.reflection import Decorators, TypeDescriptor
12
14
  from aspyx.di import injectable, Providers, ClassInstanceProvider, Environment, PostProcessor
13
15
 
@@ -51,6 +53,7 @@ class AspectTarget(ABC):
51
53
  "names",
52
54
  "patterns",
53
55
  "types",
56
+ "other",
54
57
  "decorators",
55
58
  ]
56
59
 
@@ -65,14 +68,32 @@ class AspectTarget(ABC):
65
68
  self.types = []
66
69
  self.decorators = []
67
70
 
71
+ self.other : list[AspectTarget] = []
72
+
68
73
  pass
69
74
 
70
75
  # abstract
71
76
 
72
- @abstractmethod
73
77
  def _matches(self, clazz : Type, func):
78
+ if not self._matchesSelf(clazz, func):
79
+ for target in self.other:
80
+ if target._matches(clazz, func):
81
+ return True
82
+
83
+ return False
84
+
85
+ return True
86
+
87
+ @abstractmethod
88
+ def _matchesSelf(self, clazz: Type, func):
74
89
  pass
75
90
 
91
+ # protected
92
+
93
+ def _add(self, target: AspectTarget):
94
+ self.other.append(target)
95
+ return self
96
+
76
97
  # fluent
77
98
 
78
99
  def function(self, func):
@@ -83,7 +104,7 @@ class AspectTarget(ABC):
83
104
  self._type = type
84
105
 
85
106
  return self
86
-
107
+
87
108
  def of_type(self, type: Type):
88
109
  self.types.append(type)
89
110
  return self
@@ -118,9 +139,9 @@ class ClassAspectTarget(AspectTarget):
118
139
 
119
140
  # public
120
141
 
121
- def _matches(self, clazz : Type, func):
122
- descriptor = TypeDescriptor.for_type(clazz)
123
-
142
+ def _matchesSelf(self, clazz : Type, func):
143
+ classDescriptor = TypeDescriptor.for_type(clazz)
144
+ #descriptor = TypeDescriptor.for_type(func)
124
145
  # type
125
146
 
126
147
  if len(self.types) > 0:
@@ -130,7 +151,7 @@ class ClassAspectTarget(AspectTarget):
130
151
  # decorators
131
152
 
132
153
  if len(self.decorators) > 0:
133
- if next((decorator for decorator in self.decorators if descriptor.has_decorator(decorator)), None) is None:
154
+ if next((decorator for decorator in self.decorators if classDescriptor.has_decorator(decorator)), None) is None:
134
155
  return False
135
156
 
136
157
  # names
@@ -144,8 +165,6 @@ class ClassAspectTarget(AspectTarget):
144
165
  if len(self.patterns) > 0:
145
166
  if next((pattern for pattern in self.patterns if re.fullmatch(pattern, clazz.__name__) is not None), None) is None:
146
167
  return False
147
-
148
- # yipee
149
168
 
150
169
  return True
151
170
 
@@ -174,7 +193,7 @@ class MethodAspectTarget(AspectTarget):
174
193
 
175
194
  # public
176
195
 
177
- def _matches(self, clazz : Type, func):
196
+ def _matchesSelf(self, clazz : Type, func):
178
197
  descriptor = TypeDescriptor.for_type(clazz)
179
198
 
180
199
  methodDescriptor = descriptor.get_method(func.__name__)
@@ -349,12 +368,14 @@ class Advice:
349
368
 
350
369
  __slots__ = [
351
370
  "cache",
371
+ "lock"
352
372
  ]
353
373
 
354
374
  # constructor
355
375
 
356
376
  def __init__(self):
357
377
  self.cache : Dict[Type, Dict[Callable,JoinPoints]] = dict()
378
+ self.lock = threading.RLock()
358
379
 
359
380
  # methods
360
381
 
@@ -370,20 +391,23 @@ class Advice:
370
391
 
371
392
  return aspects
372
393
 
373
- # TODO thread-safe
374
394
  def joinPoints4(self, instance, environment: Environment) -> Dict[Callable,JoinPoints]:
375
395
  clazz = type(instance)
376
396
 
377
397
  result = self.cache.get(clazz, None)
378
398
  if result is None:
379
- result = dict()
399
+ with self.lock:
400
+ result = self.cache.get(clazz, None)
401
+
402
+ if result is None:
403
+ result = dict()
380
404
 
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
405
+ for name, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
406
+ joinPoints = self.computeJoinPoints(clazz, member, environment)
407
+ if joinPoints is not None:
408
+ result[member] = joinPoints
385
409
 
386
- self.cache[clazz] = result
410
+ self.cache[clazz] = result
387
411
 
388
412
  # add around methods
389
413
 
@@ -453,57 +477,63 @@ def advice(cls):
453
477
 
454
478
  # decorators
455
479
 
456
- def _register(decorator, target: AspectTarget, func, aspectType: AspectType):
480
+ def _register(decorator, targets: list[AspectTarget], func, aspectType: AspectType):
481
+ target = targets[0]
482
+
483
+ for i in range(1, len(targets)):
484
+ target._add(targets[i])
485
+
457
486
  target.function(func).type(aspectType)
458
487
 
459
488
  Decorators.add(func, decorator, target)
460
489
 
461
- def before(target: AspectTarget):
490
+ def before(*targets: AspectTarget):
462
491
  """
463
492
  Methods decorated with @before will be executed before the target method is invoked.
464
493
  """
465
494
  def decorator(func):
466
- _register(before, target, func, AspectType.BEFORE)
495
+ _register(before, targets, func, AspectType.BEFORE)
467
496
 
468
497
  return func
469
498
 
470
499
  return decorator
471
500
 
472
- def error(target: AspectTarget):
501
+ def error(*targets: AspectTarget):
473
502
  """
474
503
  Methods decorated with @error will be executed if the target method raises an exception."""
475
504
  def decorator(func):
476
- _register(error, target, func, AspectType.ERROR)
505
+ _register(error, targets, func, AspectType.ERROR)
477
506
 
478
507
  return func
479
508
 
480
509
  return decorator
481
510
 
482
- def after(target: AspectTarget):
511
+ def after(*targets: AspectTarget):
483
512
  """
484
513
  Methods decorated with @after will be executed after the target method is invoked.
485
514
  """
486
515
  def decorator(func):
487
- _register(after, target, func, AspectType.AFTER)
516
+ _register(after, targets, func, AspectType.AFTER)
488
517
 
489
518
  return func
490
519
 
491
520
  return decorator
492
521
 
493
- def around(target: AspectTarget):
522
+ def around(*targets: AspectTarget):
494
523
  """
495
524
  Methods decorated with @around will be executed around the target method.
496
525
  Every around method must accept a single parameter of type Invocation and needs to call proceed
497
526
  on this parameter to proceed to the next around method.
498
527
  """
499
528
  def decorator(func):
500
- _register(around, target, func, AspectType.AROUND)
529
+ _register(around, targets, func, AspectType.AROUND)
501
530
 
502
531
  return func
503
532
 
504
533
  return decorator
505
534
 
506
535
  @injectable()
536
+ @order(0)
507
537
  class AdviceProcessor(PostProcessor):
508
538
  # properties
509
539
 
@@ -6,6 +6,7 @@ from typing import Optional, Type, TypeVar
6
6
  from dotenv import load_dotenv
7
7
 
8
8
  from aspyx.di import injectable, Environment, CallableProcessor, LifecycleCallable, Lifecycle, environment
9
+ from aspyx.di.di import order
9
10
  from aspyx.reflection import Decorators, DecoratorDescriptor, TypeDescriptor
10
11
 
11
12
  T = TypeVar("T")
@@ -180,6 +181,7 @@ def value(key: str, default=None):
180
181
  return decorator
181
182
 
182
183
  @injectable()
184
+ @order(9)
183
185
  class ConfigurationLifecycleCallable(LifecycleCallable):
184
186
  def __init__(self, processor: CallableProcessor, manager: ConfigurationManager):
185
187
  super().__init__(value, processor, Lifecycle.ON_INIT)
@@ -5,6 +5,7 @@ import logging
5
5
 
6
6
  from abc import abstractmethod, ABC
7
7
  from enum import Enum, auto
8
+ import threading
8
9
  from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable
9
10
 
10
11
  from aspyx.reflection import Decorators, TypeDescriptor, DecoratorDescriptor
@@ -438,12 +439,16 @@ class LifecycleProcessor(ABC):
438
439
  """
439
440
  A LifecycleProcessor is used to perform any side effects on managed objects during their lifecycle.
440
441
  """
441
- __slots__ = []
442
+ __slots__ = [
443
+ "order"
444
+ ]
442
445
 
443
446
  # constructor
444
447
 
445
448
  def __init__(self):
446
- pass
449
+ self.order = 0
450
+ if TypeDescriptor.for_type(type(self)).has_decorator(order):
451
+ self.order = TypeDescriptor.for_type(type(self)).get_decorator(order).args[0]
447
452
 
448
453
  # methods
449
454
 
@@ -596,6 +601,13 @@ def registerFactories(cls: Type):
596
601
  create_decorator = method.get_decorator(create)
597
602
  Providers.register(FunctionInstanceProvider(cls, method.method, method.returnType, create_decorator.args[0],
598
603
  create_decorator.args[1]))
604
+ def order(prio = 0):
605
+ def decorator(cls):
606
+ Decorators.add(cls, order, prio)
607
+
608
+ return cls
609
+
610
+ return decorator
599
611
 
600
612
  def injectable(eager=True, scope="singleton"):
601
613
  """
@@ -715,6 +727,7 @@ class Environment:
715
727
  ]
716
728
 
717
729
  # constructor
730
+
718
731
  def __init__(self, env: Type, parent : Optional[Environment] = None):
719
732
  """
720
733
  Creates a new Environment instance.
@@ -823,11 +836,21 @@ class Environment:
823
836
  return instance
824
837
 
825
838
  def created(self, instance: T) -> T:
839
+ def get_order(type: TypeDescriptor) -> int:
840
+ if type.has_decorator(order):
841
+ return type.get_decorator(order).args[0]
842
+ else:
843
+ return 10
844
+
826
845
  # remember lifecycle processors
827
846
 
828
847
  if isinstance(instance, LifecycleProcessor):
829
848
  self.lifecycleProcessors.append(instance)
830
849
 
850
+ # sort immediately
851
+
852
+ self.lifecycleProcessors.sort(key=lambda processor: processor.order)
853
+
831
854
  # remember instance
832
855
 
833
856
  self.instances.append(instance)
@@ -866,12 +889,17 @@ class Environment:
866
889
  class LifecycleCallable:
867
890
  __slots__ = [
868
891
  "decorator",
869
- "lifecycle"
892
+ "lifecycle",
893
+ "order"
870
894
  ]
871
895
 
872
896
  def __init__(self, decorator, processor: CallableProcessor, lifecycle: Lifecycle):
873
897
  self.decorator = decorator
874
898
  self.lifecycle = lifecycle
899
+ self.order = 0
900
+
901
+ if TypeDescriptor.for_type(type(self)).has_decorator(order):
902
+ self.order = TypeDescriptor.for_type(type(self)).get_decorator(order).args[0]
875
903
 
876
904
  processor.register(self)
877
905
 
@@ -879,6 +907,7 @@ class LifecycleCallable:
879
907
  return []
880
908
 
881
909
  @injectable()
910
+ @order(1)
882
911
  class CallableProcessor(LifecycleProcessor):
883
912
  # local classes
884
913
 
@@ -920,6 +949,12 @@ class CallableProcessor(LifecycleProcessor):
920
949
  if self.callables.get(decorator.decorator) is not None:
921
950
  result.append(CallableProcessor.MethodCall(method, decorator, self.callables[decorator.decorator]))
922
951
 
952
+ # sort according to order
953
+
954
+ result.sort(key=lambda call: call.lifecycleCallable.order)
955
+
956
+ # done
957
+
923
958
  return result
924
959
 
925
960
  def callablesFor(self, type: Type)-> list[CallableProcessor.MethodCall]:
@@ -942,6 +977,7 @@ class CallableProcessor(LifecycleProcessor):
942
977
  callable.execute(instance, environment)
943
978
 
944
979
  @injectable()
980
+ @order(1000)
945
981
  class OnInitLifecycleCallable(LifecycleCallable):
946
982
  __slots__ = []
947
983
 
@@ -949,6 +985,7 @@ class OnInitLifecycleCallable(LifecycleCallable):
949
985
  super().__init__(on_init, processor, Lifecycle.ON_INIT)
950
986
 
951
987
  @injectable()
988
+ @order(1001)
952
989
  class OnDestroyLifecycleCallable(LifecycleCallable):
953
990
  __slots__ = []
954
991
 
@@ -956,6 +993,7 @@ class OnDestroyLifecycleCallable(LifecycleCallable):
956
993
  super().__init__(on_destroy, processor, Lifecycle.ON_DESTROY)
957
994
 
958
995
  @injectable()
996
+ @order(9)
959
997
  class EnvironmentAwareLifecycleCallable(LifecycleCallable):
960
998
  __slots__ = []
961
999
 
@@ -966,6 +1004,7 @@ class EnvironmentAwareLifecycleCallable(LifecycleCallable):
966
1004
  return [environment]
967
1005
 
968
1006
  @injectable()
1007
+ @order(10)
969
1008
  class InjectLifecycleCallable(LifecycleCallable):
970
1009
  __slots__ = []
971
1010
 
@@ -1012,7 +1051,8 @@ class SingletonScope(Scope):
1012
1051
  # properties
1013
1052
 
1014
1053
  __slots__ = [
1015
- "value"
1054
+ "value",
1055
+ "lock"
1016
1056
  ]
1017
1057
 
1018
1058
  # constructor
@@ -1021,12 +1061,15 @@ class SingletonScope(Scope):
1021
1061
  super().__init__()
1022
1062
 
1023
1063
  self.value = None
1064
+ self.lock = threading.Lock()
1024
1065
 
1025
1066
  # override
1026
1067
 
1027
1068
  def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
1028
- if self.value is None: # TODO thread-safe
1029
- self.value = provider.create(environment, *argProvider())
1069
+ if self.value is None:
1070
+ with self.lock:
1071
+ if self.value is None:
1072
+ self.value = provider.create(environment, *argProvider())
1030
1073
 
1031
1074
  return self.value
1032
1075
 
@@ -6,6 +6,11 @@ from typing import Callable, get_type_hints, Type, Dict, Optional
6
6
  from weakref import WeakKeyDictionary
7
7
 
8
8
  class DecoratorDescriptor:
9
+ __slots__ = [
10
+ "decorator",
11
+ "args"
12
+ ]
13
+
9
14
  def __init__(self, decorator, *args):
10
15
  self.decorator = decorator
11
16
  self.args = args
@@ -47,14 +52,14 @@ class TypeDescriptor:
47
52
 
48
53
  def get_decorator(self, decorator):
49
54
  for dec in self.decorators:
50
- if dec.decorator == decorator:
55
+ if dec.decorator is decorator:
51
56
  return dec
52
57
 
53
58
  return None
54
59
 
55
60
  def has_decorator(self, decorator):
56
61
  for dec in self.decorators:
57
- if dec.decorator == decorator:
62
+ if dec.decorator is decorator:
58
63
  return True
59
64
 
60
65
  return False
@@ -122,7 +127,7 @@ class TypeDescriptor:
122
127
 
123
128
  def has_decorator(self, decorator) -> bool:
124
129
  for dec in self.decorators:
125
- if dec.decorator is decorator:
130
+ if dec.decorator.__name__ == decorator.__name__:
126
131
  return True
127
132
 
128
133
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 0.1.0
3
+ Version: 1.0.0
4
4
  Summary: A DI and AOP library for Python
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -32,8 +32,8 @@ Dynamic: license-file
32
32
 
33
33
  # aspyx
34
34
 
35
- ![Pylint](https://github.com/andreasernst/aspyx/actions/workflows/pylint.yml/badge.svg)
36
- ![Build Status](https://github.com/andreasernst/aspyx/actions/workflows/ci.yml/badge.svg)
35
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
36
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
37
37
 
38
38
 
39
39
  ## Table of Contents
@@ -46,8 +46,10 @@ Dynamic: license-file
46
46
  - [Environment](#environment)
47
47
  - [Definition](#definition)
48
48
  - [Retrieval](#retrieval)
49
- - [Lifecycle methods](#lifecycle-methods)
50
- - [Post Processors](#post-processors)
49
+ - [Instantiation logic](#instantiation-logic)
50
+ - [Injection Methods](#injection-methods)
51
+ - [Lifecycle Methods](#lifecycle-methods)
52
+ - [Post Processors](#post-processors)
51
53
  - [Custom scopes](#custom-scopes)
52
54
  - [AOP](#aop)
53
55
  - [Configuration](#configuration)
@@ -164,9 +166,10 @@ class Foo:
164
166
  def __init__(self):
165
167
  pass
166
168
  ```
167
- Please make sure, that the class defines a constructor, as this is required to determine injected instances.
169
+ Please make sure, that the class defines a local constructor, as this is required to determine injected instances.
170
+ All referenced types will be injected by the environemnt.
168
171
 
169
- The constructor can only define parameter types that are known as well to the container!
172
+ Only eligible types are allowed, of course!
170
173
 
171
174
 
172
175
  The decorator accepts the keyword arguments
@@ -268,15 +271,55 @@ In case of ambiguities, it will throw an exception.
268
271
 
269
272
  Please be aware, that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
270
273
 
271
- # Lifecycle methods
274
+ # Instantiation logic
272
275
 
273
- It is possible to declare methods that will be called from the container
276
+ Constructing a new instance involves a number of steps executed in this order
277
+ - Constructor call
278
+ the constructor is called with the resolved parameters
279
+ - Advice injection
280
+ All methods involving aspects are updated
281
+ - Lifecycle methods
282
+ different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
283
+ These are various injection possibilities as well as an optional final `on_init` call
284
+ - PostProcessors
285
+ Any custom post processors, that can add isde effects or modify the instances
286
+
287
+ ## Injection methods
288
+
289
+ Different decorators are implemented, that call methods with computed values
290
+
291
+ - `@inject`
292
+ the method is called with all resolved parameter types ( same as the constructor call)
293
+ - `@inject_environment`
294
+ the method is called with the creating environment as a single parameter
295
+ - `@value()`
296
+ the method is called with a resolved configuration value. Check the corresponding chapter
297
+
298
+ **Example**:
299
+ ```python
300
+ @injectable()
301
+ class Foo:
302
+ def __init__(self):
303
+ pass
304
+
305
+ @inject_environment()
306
+ def initEnvironment(self, env: Environment):
307
+ ...
308
+
309
+ @inject()
310
+ def set(self, baz: Baz) -> None:
311
+ ...
312
+ ```
313
+
314
+ ## Lifecycle methods
315
+
316
+ It is possible to mark specific lifecle methods.
274
317
  - `@on_init()`
275
318
  called after the constructor and all other injections.
276
319
  - `@on_destroy()`
277
- called after the container has been shut down
320
+ called during shutdown of the environment
278
321
 
279
- # Post Processors
322
+ ## Post Processors
280
323
 
281
324
  As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
282
325
 
@@ -5,7 +5,7 @@ import unittest
5
5
 
6
6
  from aspyx.reflection import Decorators
7
7
  from aspyx.di import injectable, Environment, environment
8
- from aspyx.di.aop import advice, before, after, around, methods, Invocation, error
8
+ from aspyx.di.aop import advice, before, after, around, methods, Invocation, error, classes
9
9
 
10
10
 
11
11
  def transactional():
@@ -22,11 +22,12 @@ class TestEnvironment:
22
22
 
23
23
 
24
24
  @injectable()
25
+ @transactional()
25
26
  class Bar:
26
27
  def __init__(self):
27
28
  pass
28
29
 
29
- @transactional()
30
+ #@transactional()
30
31
  def say(self, hello: str):
31
32
  return hello
32
33
 
@@ -89,7 +90,7 @@ class SampleAdvice:
89
90
 
90
91
  return invocation.proceed()
91
92
 
92
- @around(methods().decorated_with(transactional))
93
+ @around(methods().decorated_with(transactional), classes().decorated_with(transactional))
93
94
  def callTransactional1(self, invocation: Invocation):
94
95
  self.around_calls += 1
95
96
 
@@ -101,7 +102,7 @@ class SampleAdvice:
101
102
 
102
103
  return invocation.proceed()
103
104
 
104
- logging.basicConfig(level=logging.DEBUG)
105
+ #logging.basicConfig(level=logging.DEBUG)
105
106
 
106
107
  class TestAdvice(unittest.TestCase):
107
108
  testEnvironment = Environment(TestEnvironment)
@@ -6,6 +6,7 @@ import unittest
6
6
  from typing import Dict
7
7
 
8
8
  from aspyx.di import InjectorException, injectable, on_init, on_destroy, inject_environment, inject, Factory, create, environment, Environment, PostProcessor, factory
9
+ from aspyx.di.di import order
9
10
  from di_import import ImportedEnvironment, ImportedClass
10
11
 
11
12
  # not here
@@ -27,6 +28,7 @@ configure_logging({
27
28
 
28
29
 
29
30
  @injectable()
31
+ @order(10)
30
32
  class SamplePostProcessor(PostProcessor):
31
33
  def process(self, instance: object, environment: Environment):
32
34
  pass #print(f"created a {instance}")
@@ -220,7 +222,7 @@ class TestInject(unittest.TestCase):
220
222
 
221
223
  self.assertIsNot(ns, ns1)
222
224
 
223
- #def test_import_configurations(self):
225
+ #def test_import_configurations(self): TODO
224
226
  # env = Environment(TestEnvironment)#
225
227
 
226
228
  # imported = env.get(ImportedClass)
@@ -2,12 +2,23 @@ from __future__ import annotations
2
2
 
3
3
  import unittest
4
4
 
5
- from aspyx.reflection import TypeDescriptor
5
+ from aspyx.di import injectable
6
+ from aspyx.reflection import TypeDescriptor, Decorators
6
7
 
8
+
9
+ def transactional():
10
+ def decorator(func):
11
+ Decorators.add(func, transactional)
12
+ return func #
13
+
14
+ return decorator
15
+
16
+ @transactional()
7
17
  class Base:
8
18
  def __init__(self):
9
19
  pass
10
20
 
21
+ @transactional()
11
22
  def base(self, message: str) -> str:
12
23
  pass
13
24
 
@@ -26,7 +37,13 @@ class Derived(Base):
26
37
  pass
27
38
 
28
39
  class TestReflection(unittest.TestCase):
29
- def test(self):
40
+ def test_decorators(self):
41
+ baseDescriptor = TypeDescriptor.for_type(Base)
42
+
43
+ self.assertTrue(baseDescriptor.has_decorator(transactional))
44
+ self.assertTrue( baseDescriptor.get_method("base").has_decorator(transactional))
45
+
46
+ def test_methods(self):
30
47
  derivedDescriptor = TypeDescriptor.for_type(Derived)
31
48
 
32
49
  self.assertIsNotNone(derivedDescriptor.get_method("derived").returnType, str)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes