orionis 0.318.0__py3-none-any.whl → 0.320.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.
- orionis/container/container.py +503 -352
- orionis/container/contracts/container.py +202 -0
- orionis/container/resolver.py +28 -0
- orionis/container/validators/__init__.py +0 -0
- orionis/container/validators/implements.py +67 -0
- orionis/container/validators/is_abstract_class.py +34 -0
- orionis/container/validators/is_callable.py +30 -0
- orionis/container/validators/is_concrete_class.py +34 -0
- orionis/container/validators/is_instance.py +32 -0
- orionis/container/validators/is_not_subclass.py +32 -0
- orionis/container/validators/is_subclass.py +32 -0
- orionis/container/validators/is_valid_alias.py +37 -0
- orionis/metadata/framework.py +1 -1
- orionis/services/introspection/callables/reflection_callable.py +14 -0
- orionis/services/introspection/concretes/reflection_concrete.py +11 -0
- {orionis-0.318.0.dist-info → orionis-0.320.0.dist-info}/METADATA +1 -1
- {orionis-0.318.0.dist-info → orionis-0.320.0.dist-info}/RECORD +21 -10
- {orionis-0.318.0.dist-info → orionis-0.320.0.dist-info}/WHEEL +0 -0
- {orionis-0.318.0.dist-info → orionis-0.320.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.318.0.dist-info → orionis-0.320.0.dist-info}/top_level.txt +0 -0
- {orionis-0.318.0.dist-info → orionis-0.320.0.dist-info}/zip-safe +0 -0
orionis/container/container.py
CHANGED
@@ -1,16 +1,26 @@
|
|
1
|
+
import threading
|
1
2
|
from typing import Any, Callable
|
3
|
+
from orionis.container.contracts.container import IContainer
|
2
4
|
from orionis.container.entities.binding import Binding
|
3
5
|
from orionis.container.enums.lifetimes import Lifetime
|
4
6
|
from orionis.container.exceptions.container_exception import OrionisContainerException
|
5
7
|
from orionis.container.exceptions.type_error_exception import OrionisContainerTypeError
|
8
|
+
from orionis.container.validators.implements import ImplementsAbstractMethods
|
9
|
+
from orionis.container.validators.is_abstract_class import IsAbstractClass
|
10
|
+
from orionis.container.validators.is_callable import IsCallable
|
11
|
+
from orionis.container.validators.is_concrete_class import IsConcreteClass
|
12
|
+
from orionis.container.validators.is_instance import IsInstance
|
13
|
+
from orionis.container.validators.is_not_subclass import IsNotSubclass
|
14
|
+
from orionis.container.validators.is_subclass import IsSubclass
|
15
|
+
from orionis.container.validators.is_valid_alias import IsValidAlias
|
6
16
|
from orionis.services.introspection.abstract.reflection_abstract import ReflectionAbstract
|
7
17
|
from orionis.services.introspection.callables.reflection_callable import ReflectionCallable
|
8
18
|
from orionis.services.introspection.concretes.reflection_concrete import ReflectionConcrete
|
19
|
+
from orionis.services.introspection.dependencies.entities.resolved_dependencies import ResolvedDependency
|
9
20
|
from orionis.services.introspection.instances.reflection_instance import ReflectionInstance
|
10
21
|
from orionis.services.introspection.reflection import Reflection
|
11
|
-
import threading
|
12
22
|
|
13
|
-
class Container:
|
23
|
+
class Container(IContainer):
|
14
24
|
|
15
25
|
# Singleton instance of the container.
|
16
26
|
# This is a class variable that holds the single instance of the Container class.
|
@@ -26,7 +36,11 @@ class Container:
|
|
26
36
|
# regardless of how many times the class is instantiated.
|
27
37
|
_initialized = False
|
28
38
|
|
29
|
-
def __new__(
|
39
|
+
def __new__(
|
40
|
+
cls,
|
41
|
+
*args,
|
42
|
+
**kwargs
|
43
|
+
) -> 'Container':
|
30
44
|
"""
|
31
45
|
Creates and returns a singleton instance of the class.
|
32
46
|
|
@@ -45,339 +59,53 @@ class Container:
|
|
45
59
|
Container
|
46
60
|
The singleton instance of the class.
|
47
61
|
"""
|
62
|
+
|
63
|
+
# Check if the class has an existing instance
|
48
64
|
if cls._instance is None:
|
65
|
+
|
66
|
+
# Acquire the lock to ensure thread safety during instance creation
|
49
67
|
with cls._lock:
|
68
|
+
|
69
|
+
# If the instance is still None, create a new instance
|
50
70
|
if cls._instance is None:
|
71
|
+
|
72
|
+
# Call the superclass's __new__ method to create a new instance
|
51
73
|
cls._instance = super(Container, cls).__new__(cls)
|
74
|
+
|
75
|
+
# Return the existing instance of the class
|
52
76
|
return cls._instance
|
53
77
|
|
54
|
-
def __init__(
|
78
|
+
def __init__(
|
79
|
+
self
|
80
|
+
) -> None:
|
55
81
|
"""
|
56
82
|
Initializes a new instance of the container.
|
57
83
|
|
58
84
|
This constructor sets up the internal dictionaries for bindings and aliases,
|
59
85
|
ensuring that these are only initialized once per class. The initialization
|
60
86
|
is guarded by the `_initialized` class attribute to prevent redundant setup.
|
87
|
+
The container also registers itself as a service to allow for injection.
|
61
88
|
|
62
89
|
Notes
|
63
90
|
-----
|
64
91
|
- The `__bindings` dictionary is used to store service bindings.
|
65
92
|
- The `__aliasses` dictionary is used to store service aliases.
|
66
93
|
- Initialization occurs only once per class, regardless of the number of instances.
|
94
|
+
- The container registers itself under the IContainer interface to allow for dependency injection.
|
67
95
|
"""
|
96
|
+
|
97
|
+
# Check if the class has been initialized
|
68
98
|
if not self.__class__._initialized:
|
99
|
+
|
100
|
+
# Initialize the container's internal state
|
69
101
|
self.__bindings = {}
|
70
102
|
self.__aliasses = {}
|
71
|
-
self.__class__._initialized = True
|
72
|
-
|
73
|
-
def __dropService(
|
74
|
-
self,
|
75
|
-
abstract: Callable[..., Any] = None,
|
76
|
-
alias: str = None
|
77
|
-
) -> None:
|
78
|
-
"""
|
79
|
-
Drops a service from the container.
|
80
|
-
|
81
|
-
Parameters
|
82
|
-
----------
|
83
|
-
abstract : Callable[..., Any]
|
84
|
-
The abstract type or interface to be removed.
|
85
|
-
alias : str, optional
|
86
|
-
The alias of the service to be removed. If not provided, the service will be removed by its abstract type.
|
87
|
-
|
88
|
-
Raises
|
89
|
-
------
|
90
|
-
OrionisContainerException
|
91
|
-
If the service does not exist in the container.
|
92
|
-
"""
|
93
|
-
|
94
|
-
# If abstract is provided
|
95
|
-
if abstract:
|
96
|
-
|
97
|
-
# Remove the abstract service from the bindings if it exists
|
98
|
-
if abstract in self.__bindings:
|
99
|
-
del self.__bindings[abstract]
|
100
|
-
|
101
|
-
# Remove the default alias (module + class name) from aliases if it exists
|
102
|
-
abs_alias = ReflectionAbstract(abstract).getModuleWithClassName()
|
103
|
-
if abs_alias in self.__aliasses:
|
104
|
-
del self.__aliasses[abs_alias]
|
105
|
-
|
106
|
-
# If a custom alias is provided
|
107
|
-
if alias:
|
108
|
-
|
109
|
-
# Remove it from the aliases dictionary if it exists
|
110
|
-
if alias in self.__aliasses:
|
111
|
-
del self.__aliasses[alias]
|
112
|
-
|
113
|
-
# Remove the binding associated with the alias
|
114
|
-
if alias in self.__bindings:
|
115
|
-
del self.__bindings[alias]
|
116
|
-
|
117
|
-
def __ensureIsCallable(
|
118
|
-
self,
|
119
|
-
value: Any
|
120
|
-
) -> None:
|
121
|
-
"""
|
122
|
-
Ensures that the provided value is callable.
|
123
|
-
|
124
|
-
Parameters
|
125
|
-
----------
|
126
|
-
value : Any
|
127
|
-
The value to check.
|
128
|
-
|
129
|
-
Raises
|
130
|
-
------
|
131
|
-
OrionisContainerTypeError
|
132
|
-
If the value is not callable.
|
133
|
-
"""
|
134
|
-
|
135
|
-
if not callable(value):
|
136
|
-
raise OrionisContainerTypeError(
|
137
|
-
f"Expected a callable type, but got {type(value).__name__} instead."
|
138
|
-
)
|
139
|
-
|
140
|
-
def __ensureAliasType(
|
141
|
-
self,
|
142
|
-
value: Any
|
143
|
-
) -> None:
|
144
|
-
"""
|
145
|
-
Ensures that the provided value is a valid alias of type str and does not contain invalid characters.
|
146
|
-
|
147
|
-
Parameters
|
148
|
-
----------
|
149
|
-
value : Any
|
150
|
-
The value to check.
|
151
|
-
|
152
|
-
Raises
|
153
|
-
------
|
154
|
-
OrionisContainerTypeError
|
155
|
-
If the value is not of type str or contains invalid characters.
|
156
|
-
|
157
|
-
Notes
|
158
|
-
-----
|
159
|
-
This method validates that a given value is a string and does not contain characters
|
160
|
-
that could cause errors when resolving dependencies (e.g., whitespace, special symbols).
|
161
|
-
"""
|
162
|
-
|
163
|
-
# Check if the value is a string
|
164
|
-
if not isinstance(value, str):
|
165
|
-
raise OrionisContainerTypeError(
|
166
|
-
f"Expected a string type for alias, but got {type(value).__name__} instead."
|
167
|
-
)
|
168
|
-
|
169
|
-
# Define a set of invalid characters for aliases
|
170
|
-
invalid_chars = set(' \t\n\r\x0b\x0c!@#$%^&*()[]{};:,/<>?\\|`~"\'')
|
171
|
-
if any(char in invalid_chars for char in value):
|
172
|
-
raise OrionisContainerTypeError(
|
173
|
-
f"Alias '{value}' contains invalid characters. "
|
174
|
-
"Aliases must not contain whitespace or special symbols."
|
175
|
-
)
|
176
|
-
|
177
|
-
def __ensureAbstractClass(
|
178
|
-
self,
|
179
|
-
abstract: Callable[..., Any],
|
180
|
-
lifetime: str
|
181
|
-
) -> None:
|
182
|
-
"""
|
183
|
-
Ensures that the provided abstract is an abstract class.
|
184
|
-
|
185
|
-
Parameters
|
186
|
-
----------
|
187
|
-
abstract : Callable[..., Any]
|
188
|
-
The class intended to represent the abstract type.
|
189
|
-
lifetime : str
|
190
|
-
The service lifetime descriptor, used for error messages.
|
191
|
-
|
192
|
-
Raises
|
193
|
-
------
|
194
|
-
OrionisContainerTypeError
|
195
|
-
If the abstract class check fails.
|
196
|
-
"""
|
197
|
-
try:
|
198
|
-
ReflectionAbstract.ensureIsAbstractClass(abstract)
|
199
|
-
except Exception as e:
|
200
|
-
raise OrionisContainerTypeError(
|
201
|
-
f"Unexpected error registering {lifetime} service: {e}"
|
202
|
-
) from e
|
203
|
-
|
204
|
-
def __ensureConcreteClass(
|
205
|
-
self,
|
206
|
-
concrete: Callable[..., Any],
|
207
|
-
lifetime: str
|
208
|
-
) -> None:
|
209
|
-
"""
|
210
|
-
Ensures that the provided concrete is a concrete (non-abstract) class.
|
211
|
-
|
212
|
-
Parameters
|
213
|
-
----------
|
214
|
-
concrete : Callable[..., Any]
|
215
|
-
The class intended to represent the concrete implementation.
|
216
|
-
lifetime : str
|
217
|
-
The service lifetime descriptor, used for error messages.
|
218
|
-
|
219
|
-
Raises
|
220
|
-
------
|
221
|
-
OrionisContainerTypeError
|
222
|
-
If the concrete class check fails.
|
223
|
-
"""
|
224
|
-
try:
|
225
|
-
ReflectionConcrete.ensureIsConcreteClass(concrete)
|
226
|
-
except Exception as e:
|
227
|
-
raise OrionisContainerTypeError(
|
228
|
-
f"Unexpected error registering {lifetime} service: {e}"
|
229
|
-
) from e
|
230
|
-
|
231
|
-
def __ensureIsSubclass(
|
232
|
-
self,
|
233
|
-
abstract: Callable[..., Any],
|
234
|
-
concrete: Callable[..., Any]
|
235
|
-
) -> None:
|
236
|
-
"""
|
237
|
-
Validates that the concrete class is a subclass of the provided abstract class.
|
238
|
-
|
239
|
-
Parameters
|
240
|
-
----------
|
241
|
-
abstract : Callable[..., Any]
|
242
|
-
The abstract base class or interface.
|
243
|
-
concrete : Callable[..., Any]
|
244
|
-
The concrete implementation class to check.
|
245
|
-
|
246
|
-
Raises
|
247
|
-
------
|
248
|
-
OrionisContainerException
|
249
|
-
If the concrete class is NOT a subclass of the abstract class.
|
250
|
-
|
251
|
-
Notes
|
252
|
-
-----
|
253
|
-
This method ensures that the concrete implementation inherits from the abstract class,
|
254
|
-
which is required for proper dependency injection and interface enforcement.
|
255
|
-
"""
|
256
|
-
if not issubclass(concrete, abstract):
|
257
|
-
raise OrionisContainerException(
|
258
|
-
"The concrete class must inherit from the provided abstract class. "
|
259
|
-
"Please ensure that the concrete class is a subclass of the specified abstract class."
|
260
|
-
)
|
261
|
-
|
262
|
-
def __ensureIsNotSubclass(
|
263
|
-
self,
|
264
|
-
abstract: Callable[..., Any],
|
265
|
-
concrete: Callable[..., Any]
|
266
|
-
) -> None:
|
267
|
-
"""
|
268
|
-
Validates that the concrete class is NOT a subclass of the provided abstract class.
|
269
|
-
|
270
|
-
Parameters
|
271
|
-
----------
|
272
|
-
abstract : Callable[..., Any]
|
273
|
-
The abstract base class or interface.
|
274
|
-
concrete : Callable[..., Any]
|
275
|
-
The concrete implementation class to check.
|
276
|
-
|
277
|
-
Raises
|
278
|
-
------
|
279
|
-
OrionisContainerException
|
280
|
-
If the concrete class IS a subclass of the abstract class.
|
281
|
-
|
282
|
-
Notes
|
283
|
-
-----
|
284
|
-
This method ensures that the concrete implementation does NOT inherit from the abstract class.
|
285
|
-
"""
|
286
|
-
if issubclass(concrete, abstract):
|
287
|
-
raise OrionisContainerException(
|
288
|
-
"The concrete class must NOT inherit from the provided abstract class. "
|
289
|
-
"Please ensure that the concrete class is not a subclass of the specified abstract class."
|
290
|
-
)
|
291
|
-
|
292
|
-
def __ensureInstance(
|
293
|
-
self,
|
294
|
-
instance: Any
|
295
|
-
) -> None:
|
296
|
-
"""
|
297
|
-
Ensures that the provided object is a valid instance.
|
298
|
-
|
299
|
-
Parameters
|
300
|
-
----------
|
301
|
-
instance : Any
|
302
|
-
The object to be validated as an instance.
|
303
|
-
|
304
|
-
Raises
|
305
|
-
------
|
306
|
-
OrionisContainerTypeError
|
307
|
-
If the provided object is not a valid instance.
|
308
|
-
|
309
|
-
Notes
|
310
|
-
-----
|
311
|
-
This method uses ReflectionInstance to verify that the given object
|
312
|
-
is a proper instance (not a class or abstract type). If the check fails,
|
313
|
-
an OrionisContainerTypeError is raised with a descriptive message.
|
314
|
-
"""
|
315
|
-
try:
|
316
|
-
ReflectionInstance.ensureIsInstance(instance)
|
317
|
-
except Exception as e:
|
318
|
-
raise OrionisContainerTypeError(
|
319
|
-
f"Error registering instance: {e}"
|
320
|
-
) from e
|
321
|
-
|
322
|
-
def __ensureImplementation(
|
323
|
-
self,
|
324
|
-
*,
|
325
|
-
abstract: Callable[..., Any] = None,
|
326
|
-
concrete: Callable[..., Any] = None,
|
327
|
-
instance: Any = None
|
328
|
-
) -> None:
|
329
|
-
"""
|
330
|
-
Ensures that a concrete class or instance implements all abstract methods defined in an abstract class.
|
331
|
-
|
332
|
-
Parameters
|
333
|
-
----------
|
334
|
-
abstract : Callable[..., Any]
|
335
|
-
The abstract class containing abstract methods.
|
336
|
-
concrete : Callable[..., Any], optional
|
337
|
-
The concrete class that should implement the abstract methods.
|
338
|
-
instance : Any, optional
|
339
|
-
The instance that should implement the abstract methods.
|
340
|
-
|
341
|
-
Raises
|
342
|
-
------
|
343
|
-
OrionisContainerException
|
344
|
-
If the concrete class or instance does not implement all abstract methods defined in the abstract class.
|
345
|
-
|
346
|
-
Notes
|
347
|
-
-----
|
348
|
-
This method checks that all abstract methods in the given abstract class are implemented
|
349
|
-
in the provided concrete class or instance. If any methods are missing, an exception is raised with
|
350
|
-
details about the missing implementations.
|
351
|
-
"""
|
352
|
-
if abstract is None:
|
353
|
-
raise OrionisContainerException("Abstract class must be provided for implementation check.")
|
354
|
-
|
355
|
-
abstract_methods = getattr(abstract, '__abstractmethods__', set())
|
356
|
-
if not abstract_methods:
|
357
|
-
raise OrionisContainerException(
|
358
|
-
f"The abstract class '{abstract.__name__}' does not define any abstract methods. "
|
359
|
-
"An abstract class must have at least one abstract method."
|
360
|
-
)
|
361
|
-
|
362
|
-
target = concrete if concrete is not None else instance
|
363
|
-
if target is None:
|
364
|
-
raise OrionisContainerException("Either concrete class or instance must be provided for implementation check.")
|
365
|
-
|
366
|
-
target_class = target if Reflection.isClass(target) else target.__class__
|
367
|
-
target_name = target_class.__name__
|
368
|
-
abstract_name = abstract.__name__
|
369
103
|
|
370
|
-
|
371
|
-
|
372
|
-
if not hasattr(target, str(method).replace(f"_{abstract_name}", f"_{target_name}")):
|
373
|
-
not_implemented.append(method)
|
104
|
+
# Set the initialized flag to True to prevent re-initialization
|
105
|
+
self.__class__._initialized = True
|
374
106
|
|
375
|
-
|
376
|
-
|
377
|
-
raise OrionisContainerException(
|
378
|
-
f"'{target_name}' does not implement the following abstract methods defined in '{abstract_name}':{formatted_methods}\n"
|
379
|
-
"Please ensure that all abstract methods are implemented."
|
380
|
-
)
|
107
|
+
# Register the container itself as a service
|
108
|
+
self.instance(IContainer, self)
|
381
109
|
|
382
110
|
def transient(
|
383
111
|
self,
|
@@ -419,28 +147,28 @@ class Container:
|
|
419
147
|
"""
|
420
148
|
|
421
149
|
# Ensure that abstract is an abstract class
|
422
|
-
|
150
|
+
IsAbstractClass(abstract, Lifetime.TRANSIENT)
|
423
151
|
|
424
152
|
# Ensure that concrete is a concrete class
|
425
|
-
|
153
|
+
IsConcreteClass(concrete, Lifetime.TRANSIENT)
|
426
154
|
|
427
155
|
# Ensure that concrete is NOT a subclass of abstract
|
428
156
|
if enforce_decoupling:
|
429
|
-
|
157
|
+
IsNotSubclass(abstract, concrete)
|
430
158
|
|
431
159
|
# Validate that concrete is a subclass of abstract
|
432
160
|
else:
|
433
|
-
|
161
|
+
IsSubclass(abstract, concrete)
|
434
162
|
|
435
163
|
# Ensure implementation
|
436
|
-
|
164
|
+
ImplementsAbstractMethods(
|
437
165
|
abstract=abstract,
|
438
166
|
concrete=concrete
|
439
167
|
)
|
440
168
|
|
441
169
|
# Ensure that the alias is a valid string if provided
|
442
170
|
if alias:
|
443
|
-
|
171
|
+
IsValidAlias(alias)
|
444
172
|
|
445
173
|
# Extract the module and class name for the alias
|
446
174
|
else:
|
@@ -448,7 +176,7 @@ class Container:
|
|
448
176
|
alias = rf_asbtract.getModuleWithClassName()
|
449
177
|
|
450
178
|
# If the service is already registered, drop it
|
451
|
-
self.
|
179
|
+
self.drop(abstract, alias)
|
452
180
|
|
453
181
|
# Register the service with transient lifetime
|
454
182
|
self.__bindings[abstract] = Binding(
|
@@ -504,34 +232,34 @@ class Container:
|
|
504
232
|
"""
|
505
233
|
|
506
234
|
# Ensure that abstract is an abstract class
|
507
|
-
|
235
|
+
IsAbstractClass(abstract, Lifetime.SINGLETON)
|
508
236
|
|
509
237
|
# Ensure that concrete is a concrete class
|
510
|
-
|
238
|
+
IsConcreteClass(concrete, Lifetime.SINGLETON)
|
511
239
|
|
512
240
|
# Ensure that concrete is NOT a subclass of abstract
|
513
241
|
if enforce_decoupling:
|
514
|
-
|
242
|
+
IsNotSubclass(abstract, concrete)
|
515
243
|
|
516
244
|
# Validate that concrete is a subclass of abstract
|
517
245
|
else:
|
518
|
-
|
246
|
+
IsSubclass(abstract, concrete)
|
519
247
|
|
520
248
|
# Ensure implementation
|
521
|
-
|
249
|
+
ImplementsAbstractMethods(
|
522
250
|
abstract=abstract,
|
523
251
|
concrete=concrete
|
524
252
|
)
|
525
253
|
|
526
254
|
# Ensure that the alias is a valid string if provided
|
527
255
|
if alias:
|
528
|
-
|
256
|
+
IsValidAlias(alias)
|
529
257
|
else:
|
530
258
|
rf_asbtract = ReflectionAbstract(abstract)
|
531
259
|
alias = rf_asbtract.getModuleWithClassName()
|
532
260
|
|
533
261
|
# If the service is already registered, drop it
|
534
|
-
self.
|
262
|
+
self.drop(abstract, alias)
|
535
263
|
|
536
264
|
# Register the service with singleton lifetime
|
537
265
|
self.__bindings[abstract] = Binding(
|
@@ -587,34 +315,34 @@ class Container:
|
|
587
315
|
"""
|
588
316
|
|
589
317
|
# Ensure that abstract is an abstract class
|
590
|
-
|
318
|
+
IsAbstractClass(abstract, Lifetime.SCOPED)
|
591
319
|
|
592
320
|
# Ensure that concrete is a concrete class
|
593
|
-
|
321
|
+
IsConcreteClass(concrete, Lifetime.SCOPED)
|
594
322
|
|
595
323
|
# Ensure that concrete is NOT a subclass of abstract
|
596
324
|
if enforce_decoupling:
|
597
|
-
|
325
|
+
IsNotSubclass(abstract, concrete)
|
598
326
|
|
599
327
|
# Validate that concrete is a subclass of abstract
|
600
328
|
else:
|
601
|
-
|
329
|
+
IsSubclass(abstract, concrete)
|
602
330
|
|
603
331
|
# Ensure implementation
|
604
|
-
|
332
|
+
ImplementsAbstractMethods(
|
605
333
|
abstract=abstract,
|
606
334
|
concrete=concrete
|
607
335
|
)
|
608
336
|
|
609
337
|
# Ensure that the alias is a valid string if provided
|
610
338
|
if alias:
|
611
|
-
|
339
|
+
IsValidAlias(alias)
|
612
340
|
else:
|
613
341
|
rf_asbtract = ReflectionAbstract(abstract)
|
614
342
|
alias = rf_asbtract.getModuleWithClassName()
|
615
343
|
|
616
344
|
# If the service is already registered, drop it
|
617
|
-
self.
|
345
|
+
self.drop(abstract, alias)
|
618
346
|
|
619
347
|
# Register the service with scoped lifetime
|
620
348
|
self.__bindings[abstract] = Binding(
|
@@ -668,34 +396,34 @@ class Container:
|
|
668
396
|
"""
|
669
397
|
|
670
398
|
# Ensure that the abstract is an abstract class
|
671
|
-
|
399
|
+
IsAbstractClass(abstract, f"Instance {Lifetime.SINGLETON}")
|
672
400
|
|
673
401
|
# Ensure that the instance is a valid instance
|
674
|
-
|
402
|
+
IsInstance(instance)
|
675
403
|
|
676
404
|
# Ensure that instance is NOT a subclass of abstract
|
677
405
|
if enforce_decoupling:
|
678
|
-
|
406
|
+
IsNotSubclass(abstract, instance.__class__)
|
679
407
|
|
680
408
|
# Validate that instance is a subclass of abstract
|
681
409
|
else:
|
682
|
-
|
410
|
+
IsSubclass(abstract, instance.__class__)
|
683
411
|
|
684
412
|
# Ensure implementation
|
685
|
-
|
413
|
+
ImplementsAbstractMethods(
|
686
414
|
abstract=abstract,
|
687
415
|
instance=instance
|
688
416
|
)
|
689
417
|
|
690
418
|
# Ensure that the alias is a valid string if provided
|
691
419
|
if alias:
|
692
|
-
|
420
|
+
IsValidAlias(alias)
|
693
421
|
else:
|
694
422
|
rf_asbtract = ReflectionAbstract(abstract)
|
695
423
|
alias = rf_asbtract.getModuleWithClassName()
|
696
424
|
|
697
425
|
# If the service is already registered, drop it
|
698
|
-
self.
|
426
|
+
self.drop(abstract, alias)
|
699
427
|
|
700
428
|
# Register the instance with the abstract type
|
701
429
|
self.__bindings[abstract] = Binding(
|
@@ -715,7 +443,7 @@ class Container:
|
|
715
443
|
def function(
|
716
444
|
self,
|
717
445
|
alias: str,
|
718
|
-
|
446
|
+
fn: Callable[..., Any],
|
719
447
|
*,
|
720
448
|
lifetime: Lifetime = Lifetime.TRANSIENT
|
721
449
|
) -> bool:
|
@@ -726,7 +454,7 @@ class Container:
|
|
726
454
|
----------
|
727
455
|
alias : str
|
728
456
|
The alias to register the function under.
|
729
|
-
|
457
|
+
fn : Callable[..., Any]
|
730
458
|
The function or factory to register.
|
731
459
|
lifetime : Lifetime, optional
|
732
460
|
The lifetime of the function registration (default is TRANSIENT).
|
@@ -760,13 +488,13 @@ class Container:
|
|
760
488
|
)
|
761
489
|
|
762
490
|
# Ensure that the alias is a valid string
|
763
|
-
|
491
|
+
IsValidAlias(alias)
|
764
492
|
|
765
493
|
# Validate that the function is callable
|
766
|
-
|
494
|
+
IsCallable(fn)
|
767
495
|
|
768
496
|
# Inspect the function signature
|
769
|
-
params = ReflectionCallable(
|
497
|
+
params = ReflectionCallable(fn).getDependencies()
|
770
498
|
|
771
499
|
# If the function requires arguments, only allow TRANSIENT
|
772
500
|
if (len(params.resolved) + len(params.unresolved)) > 0 and lifetime != Lifetime.TRANSIENT:
|
@@ -775,11 +503,11 @@ class Container:
|
|
775
503
|
)
|
776
504
|
|
777
505
|
# If the service is already registered, drop it
|
778
|
-
self.
|
506
|
+
self.drop(None, alias)
|
779
507
|
|
780
508
|
# Register the function with the specified alias and lifetime
|
781
509
|
self.__bindings[alias] = Binding(
|
782
|
-
function=
|
510
|
+
function=fn,
|
783
511
|
lifetime=lifetime,
|
784
512
|
alias=alias
|
785
513
|
)
|
@@ -816,11 +544,104 @@ class Container:
|
|
816
544
|
or abstract_or_alias in self.__aliasses
|
817
545
|
)
|
818
546
|
|
547
|
+
def __getService(
|
548
|
+
self,
|
549
|
+
abstract_or_alias: Any
|
550
|
+
) -> Binding:
|
551
|
+
"""
|
552
|
+
Retrieves the binding for the requested abstract type or alias.
|
553
|
+
|
554
|
+
Parameters
|
555
|
+
----------
|
556
|
+
abstract_or_alias : Any
|
557
|
+
The abstract class, interface, or alias (str) to retrieve.
|
558
|
+
|
559
|
+
Returns
|
560
|
+
-------
|
561
|
+
Binding
|
562
|
+
The binding associated with the requested abstract type or alias.
|
563
|
+
"""
|
564
|
+
return self.__bindings.get(abstract_or_alias) or self.__aliasses.get(abstract_or_alias)
|
565
|
+
|
566
|
+
def __getFirstService(
|
567
|
+
self,
|
568
|
+
abstract_or_aliasses: list
|
569
|
+
) -> Binding:
|
570
|
+
"""
|
571
|
+
Retrieves the first binding from a list of abstract types or aliases.
|
572
|
+
|
573
|
+
Parameters
|
574
|
+
----------
|
575
|
+
abstract_or_aliasses : list
|
576
|
+
A list of abstract classes, interfaces, or aliases (str) to retrieve.
|
577
|
+
|
578
|
+
Returns
|
579
|
+
-------
|
580
|
+
Binding
|
581
|
+
The first binding found in the container for the provided abstract types or aliases.
|
582
|
+
"""
|
583
|
+
for item in abstract_or_aliasses:
|
584
|
+
binding = self.__getService(item)
|
585
|
+
if binding:
|
586
|
+
return binding
|
587
|
+
return None
|
588
|
+
|
589
|
+
def drop(
|
590
|
+
self,
|
591
|
+
abstract: Callable[..., Any] = None,
|
592
|
+
alias: str = None
|
593
|
+
) -> None:
|
594
|
+
"""
|
595
|
+
Drops a service from the container by removing its bindings and aliases.
|
596
|
+
This method allows removing registered services from the dependency injection container,
|
597
|
+
either by their abstract type or by their alias. When a service is dropped,
|
598
|
+
all its bindings and aliases are removed from the container.
|
599
|
+
|
600
|
+
Warning
|
601
|
+
-------
|
602
|
+
Using this method irresponsibly can severely damage the system's logic.
|
603
|
+
Only use it when you are certain about the consequences, as removing
|
604
|
+
critical services may lead to system failures and unexpected behavior.
|
605
|
+
abstract : Callable[..., Any], optional
|
606
|
+
The abstract type or interface to be removed from the container.
|
607
|
+
If provided, both the binding and the default alias for this type will be removed.
|
608
|
+
The alias of the service to be removed. If provided, both the alias entry
|
609
|
+
and any associated binding will be removed.
|
610
|
+
|
611
|
+
Notes
|
612
|
+
-----
|
613
|
+
At least one parameter (abstract or alias) must be provided for the method to take effect.
|
614
|
+
If both are provided, both will be processed independently.
|
615
|
+
"""
|
616
|
+
|
617
|
+
# If abstract is provided
|
618
|
+
if abstract:
|
619
|
+
|
620
|
+
# Remove the abstract service from the bindings if it exists
|
621
|
+
if abstract in self.__bindings:
|
622
|
+
del self.__bindings[abstract]
|
623
|
+
|
624
|
+
# Remove the default alias (module + class name) from aliases if it exists
|
625
|
+
abs_alias = ReflectionAbstract(abstract).getModuleWithClassName()
|
626
|
+
if abs_alias in self.__aliasses:
|
627
|
+
del self.__aliasses[abs_alias]
|
628
|
+
|
629
|
+
# If a custom alias is provided
|
630
|
+
if alias:
|
631
|
+
|
632
|
+
# Remove it from the aliases dictionary if it exists
|
633
|
+
if alias in self.__aliasses:
|
634
|
+
del self.__aliasses[alias]
|
635
|
+
|
636
|
+
# Remove the binding associated with the alias
|
637
|
+
if alias in self.__bindings:
|
638
|
+
del self.__bindings[alias]
|
639
|
+
|
819
640
|
def make(
|
820
641
|
self,
|
821
642
|
abstract_or_alias: Any,
|
822
|
-
*args,
|
823
|
-
**kwargs
|
643
|
+
*args: tuple,
|
644
|
+
**kwargs: dict
|
824
645
|
) -> Any:
|
825
646
|
"""
|
826
647
|
Resolves and returns an instance of the requested service.
|
@@ -844,11 +665,341 @@ class Container:
|
|
844
665
|
OrionisContainerException
|
845
666
|
If the requested service is not registered in the container.
|
846
667
|
"""
|
847
|
-
|
668
|
+
# Retrieve the binding for the requested abstract or alias
|
669
|
+
binding = self.__getService(abstract_or_alias)
|
670
|
+
|
671
|
+
# Check if the requested service is registered in the container
|
672
|
+
if not binding:
|
848
673
|
raise OrionisContainerException(
|
849
674
|
f"The requested service '{abstract_or_alias}' is not registered in the container."
|
850
675
|
)
|
851
676
|
|
852
|
-
binding
|
677
|
+
# Handle based on binding type and lifetime
|
678
|
+
if binding.lifetime == Lifetime.TRANSIENT:
|
679
|
+
return self.__resolveTransient(binding, *args, **kwargs)
|
680
|
+
elif binding.lifetime == Lifetime.SINGLETON:
|
681
|
+
return self.__resolveSingleton(binding, *args, **kwargs)
|
682
|
+
elif binding.lifetime == Lifetime.SCOPED:
|
683
|
+
# TODO: Implement scoped lifetime resolution
|
684
|
+
raise OrionisContainerException(
|
685
|
+
"Scoped lifetime resolution is not yet implemented."
|
686
|
+
)
|
687
|
+
|
688
|
+
def __resolveTransient(self, binding: Binding, *args, **kwargs) -> Any:
|
689
|
+
"""
|
690
|
+
Resolves a service with transient lifetime.
|
691
|
+
|
692
|
+
Parameters
|
693
|
+
----------
|
694
|
+
binding : Binding
|
695
|
+
The binding to resolve.
|
696
|
+
*args : tuple
|
697
|
+
Positional arguments to pass to the constructor.
|
698
|
+
**kwargs : dict
|
699
|
+
Keyword arguments to pass to the constructor.
|
853
700
|
|
854
|
-
|
701
|
+
Returns
|
702
|
+
-------
|
703
|
+
Any
|
704
|
+
A new instance of the requested service.
|
705
|
+
"""
|
706
|
+
|
707
|
+
# Check if the binding has a concrete class or function defined
|
708
|
+
if binding.concrete:
|
709
|
+
if args or kwargs:
|
710
|
+
return self.__instantiateConcreteWithArgs(binding.concrete, *args, **kwargs)
|
711
|
+
else:
|
712
|
+
return self.__instantiateConcreteReflective(binding.concrete)
|
713
|
+
|
714
|
+
# If the binding has a function defined
|
715
|
+
elif binding.function:
|
716
|
+
if args or kwargs:
|
717
|
+
return self.__instantiateCallableWithArgs(binding.function, *args, **kwargs)
|
718
|
+
else:
|
719
|
+
return self.__instantiateCallableReflective(binding.function)
|
720
|
+
|
721
|
+
# If neither concrete class nor function is defined
|
722
|
+
else:
|
723
|
+
raise OrionisContainerException(
|
724
|
+
"Cannot resolve transient binding: neither a concrete class nor a function is defined."
|
725
|
+
)
|
726
|
+
|
727
|
+
def __resolveSingleton(self, binding: Binding, *args, **kwargs) -> Any:
|
728
|
+
"""
|
729
|
+
Resolves a service with singleton lifetime.
|
730
|
+
|
731
|
+
Parameters
|
732
|
+
----------
|
733
|
+
binding : Binding
|
734
|
+
The binding to resolve.
|
735
|
+
*args : tuple
|
736
|
+
Positional arguments to pass to the constructor (only used if instance doesn't exist yet).
|
737
|
+
**kwargs : dict
|
738
|
+
Keyword arguments to pass to the constructor (only used if instance doesn't exist yet).
|
739
|
+
|
740
|
+
Returns
|
741
|
+
-------
|
742
|
+
Any
|
743
|
+
The singleton instance of the requested service.
|
744
|
+
"""
|
745
|
+
# Return existing instance if available
|
746
|
+
if binding.instance:
|
747
|
+
return binding.instance
|
748
|
+
|
749
|
+
# Create instance if needed
|
750
|
+
if binding.concrete:
|
751
|
+
if args or kwargs:
|
752
|
+
binding.instance = self.__instantiateConcreteWithArgs(binding.concrete, *args, **kwargs)
|
753
|
+
else:
|
754
|
+
binding.instance = self.__instantiateConcreteReflective(binding.concrete)
|
755
|
+
return binding.instance
|
756
|
+
|
757
|
+
# If the binding has a function defined
|
758
|
+
elif binding.function:
|
759
|
+
if args or kwargs:
|
760
|
+
result = self.__instantiateCallableWithArgs(binding.function, *args, **kwargs)
|
761
|
+
else:
|
762
|
+
result = self.__instantiateCallableReflective(binding.function)
|
763
|
+
|
764
|
+
# Store the result directly as the singleton instance
|
765
|
+
# We don't automatically invoke factory function results anymore
|
766
|
+
binding.instance = result
|
767
|
+
return binding.instance
|
768
|
+
|
769
|
+
# If neither concrete class nor function is defined
|
770
|
+
else:
|
771
|
+
raise OrionisContainerException(
|
772
|
+
"Cannot resolve singleton binding: neither a concrete class, instance, nor function is defined."
|
773
|
+
)
|
774
|
+
|
775
|
+
def __instantiateConcreteWithArgs(self, concrete: Callable[..., Any], *args, **kwargs) -> Any:
|
776
|
+
"""
|
777
|
+
Instantiates a concrete class with the provided arguments.
|
778
|
+
|
779
|
+
Parameters
|
780
|
+
----------
|
781
|
+
concrete : Callable[..., Any]
|
782
|
+
Class to instantiate.
|
783
|
+
*args : tuple
|
784
|
+
Positional arguments to pass to the constructor.
|
785
|
+
**kwargs : dict
|
786
|
+
Keyword arguments to pass to the constructor.
|
787
|
+
|
788
|
+
Returns
|
789
|
+
-------
|
790
|
+
object
|
791
|
+
A new instance of the specified concrete class.
|
792
|
+
"""
|
793
|
+
|
794
|
+
# try to instantiate the concrete class with the provided arguments
|
795
|
+
try:
|
796
|
+
|
797
|
+
# If the concrete is a class, instantiate it directly
|
798
|
+
return concrete(*args, **kwargs)
|
799
|
+
|
800
|
+
except TypeError as e:
|
801
|
+
|
802
|
+
# If instantiation fails, use ReflectionConcrete to get class name and constructor signature
|
803
|
+
rf_concrete = ReflectionConcrete(concrete)
|
804
|
+
class_name = rf_concrete.getClassName()
|
805
|
+
signature = rf_concrete.getConstructorSignature()
|
806
|
+
|
807
|
+
# Raise an exception with detailed information about the failure
|
808
|
+
raise OrionisContainerException(
|
809
|
+
f"Failed to instantiate [{class_name}] with the provided arguments: {e}\n"
|
810
|
+
f"Expected constructor signature: [{signature}]"
|
811
|
+
) from e
|
812
|
+
|
813
|
+
def __instantiateCallableWithArgs(self, fn: Callable[..., Any], *args, **kwargs) -> Any:
|
814
|
+
"""
|
815
|
+
Invokes a callable with the provided arguments.
|
816
|
+
|
817
|
+
Parameters
|
818
|
+
----------
|
819
|
+
fn : Callable[..., Any]
|
820
|
+
The callable to invoke.
|
821
|
+
*args : tuple
|
822
|
+
Positional arguments to pass to the callable.
|
823
|
+
**kwargs : dict
|
824
|
+
Keyword arguments to pass to the callable.
|
825
|
+
|
826
|
+
Returns
|
827
|
+
-------
|
828
|
+
Any
|
829
|
+
The result of the callable.
|
830
|
+
"""
|
831
|
+
|
832
|
+
# Try to invoke the callable with the provided arguments
|
833
|
+
try:
|
834
|
+
|
835
|
+
# If the callable is a function, invoke it directly
|
836
|
+
return fn(*args, **kwargs)
|
837
|
+
|
838
|
+
except TypeError as e:
|
839
|
+
|
840
|
+
# If invocation fails, use ReflectionCallable to get function name and signature
|
841
|
+
rf_callable = ReflectionCallable(fn)
|
842
|
+
function_name = rf_callable.getName()
|
843
|
+
signature = rf_callable.getSignature()
|
844
|
+
|
845
|
+
# Raise an exception with detailed information about the failure
|
846
|
+
raise OrionisContainerException(
|
847
|
+
f"Failed to invoke function [{function_name}] with the provided arguments: {e}\n"
|
848
|
+
f"Expected function signature: [{signature}]"
|
849
|
+
) from e
|
850
|
+
|
851
|
+
def __instantiateConcreteReflective(self, concrete: Callable[..., Any]) -> Any:
|
852
|
+
"""
|
853
|
+
Instantiates a concrete class reflectively, resolving its dependencies from the container.
|
854
|
+
|
855
|
+
Parameters
|
856
|
+
----------
|
857
|
+
concrete : Callable[..., Any]
|
858
|
+
The concrete class to instantiate.
|
859
|
+
|
860
|
+
Returns
|
861
|
+
-------
|
862
|
+
Any
|
863
|
+
A new instance of the concrete class.
|
864
|
+
"""
|
865
|
+
# Resolve dependencies for the concrete class
|
866
|
+
params = self.__resolveDependencies(concrete, is_class=True)
|
867
|
+
|
868
|
+
# Instantiate the concrete class with resolved dependencies
|
869
|
+
return concrete(**params)
|
870
|
+
|
871
|
+
def __instantiateCallableReflective(self, fn: Callable[..., Any]) -> Any:
|
872
|
+
"""
|
873
|
+
Invokes a callable reflectively, resolving its dependencies from the container.
|
874
|
+
|
875
|
+
Parameters
|
876
|
+
----------
|
877
|
+
fn : Callable[..., Any]
|
878
|
+
The callable to invoke.
|
879
|
+
|
880
|
+
Returns
|
881
|
+
-------
|
882
|
+
Any
|
883
|
+
The result of the callable.
|
884
|
+
"""
|
885
|
+
|
886
|
+
# Resolve dependencies for the callable
|
887
|
+
params = self.__resolveDependencies(fn, is_class=False)
|
888
|
+
|
889
|
+
# Invoke the callable with resolved dependencies
|
890
|
+
return fn(**params)
|
891
|
+
|
892
|
+
def __resolveDependencies(
|
893
|
+
self,
|
894
|
+
target: Callable[..., Any],
|
895
|
+
*,
|
896
|
+
is_class: bool = False
|
897
|
+
) -> dict:
|
898
|
+
"""
|
899
|
+
Resolves dependencies for a target callable or class.
|
900
|
+
|
901
|
+
Parameters
|
902
|
+
----------
|
903
|
+
target : Callable[..., Any]
|
904
|
+
The target callable or class whose dependencies to resolve.
|
905
|
+
is_class : bool, optional
|
906
|
+
Whether the target is a class (True) or a callable (False).
|
907
|
+
|
908
|
+
Returns
|
909
|
+
-------
|
910
|
+
dict
|
911
|
+
A dictionary of resolved dependencies.
|
912
|
+
"""
|
913
|
+
try:
|
914
|
+
|
915
|
+
# Use ReflectionConcrete for classes and ReflectionCallable for callables
|
916
|
+
if is_class:
|
917
|
+
reflection = ReflectionConcrete(target)
|
918
|
+
dependencies = reflection.getConstructorDependencies()
|
919
|
+
name = reflection.getClassName()
|
920
|
+
|
921
|
+
# If the target is a callable, use ReflectionCallable
|
922
|
+
else:
|
923
|
+
reflection = ReflectionCallable(target)
|
924
|
+
dependencies = reflection.getDependencies()
|
925
|
+
name = reflection.getName()
|
926
|
+
|
927
|
+
# Check for unresolved dependencies
|
928
|
+
if dependencies.unresolved:
|
929
|
+
unresolved_args = ', '.join(dependencies.unresolved)
|
930
|
+
raise OrionisContainerException(
|
931
|
+
f"Cannot resolve '{name}' because the following required arguments are missing: [{unresolved_args}]."
|
932
|
+
)
|
933
|
+
|
934
|
+
# Resolve dependencies
|
935
|
+
params = {}
|
936
|
+
for param_name, dep in dependencies.resolved.items():
|
937
|
+
|
938
|
+
# If the dependency is a ResolvedDependency, resolve it
|
939
|
+
if isinstance(dep, ResolvedDependency):
|
940
|
+
|
941
|
+
# If the dependency is a built-in type, raise an exception
|
942
|
+
if dep.module_name == 'builtins':
|
943
|
+
raise OrionisContainerException(
|
944
|
+
f"Cannot resolve '{name}' because parameter '{param_name}' depends on built-in type '{dep.type.__name__}'."
|
945
|
+
)
|
946
|
+
|
947
|
+
# Try to resolve from container
|
948
|
+
service = self.__getFirstService([dep.type, dep.full_class_path])
|
949
|
+
if service:
|
950
|
+
params[param_name] = self.make(service.alias)
|
951
|
+
|
952
|
+
# Try to instantiate directly if it's a concrete class
|
953
|
+
elif ReflectionConcrete.isConcreteClass(dep.type):
|
954
|
+
params[param_name] = dep.type(**self.__resolveDependencies(dep.type, is_class=True))
|
955
|
+
|
956
|
+
# Try to call directly if it's a callable
|
957
|
+
elif callable(dep.type) and not isinstance(dep.type, type):
|
958
|
+
params[param_name] = dep.type(**self.__resolveDependencies(dep.type, is_class=False))
|
959
|
+
|
960
|
+
# If the dependency cannot be resolved, raise an exception
|
961
|
+
else:
|
962
|
+
raise OrionisContainerException(
|
963
|
+
f"Cannot resolve dependency '{param_name}' of type '{dep.type.__name__}' for '{name}'."
|
964
|
+
)
|
965
|
+
else:
|
966
|
+
# Use default value
|
967
|
+
params[param_name] = dep
|
968
|
+
|
969
|
+
# Return the resolved parameters
|
970
|
+
return params
|
971
|
+
|
972
|
+
except ImportError as e:
|
973
|
+
|
974
|
+
# Extract module name from the error message if possible
|
975
|
+
import_msg = str(e)
|
976
|
+
module_name = target.__module__ if hasattr(target, '__module__') else "unknown module"
|
977
|
+
|
978
|
+
# Check for potential circular import patterns
|
979
|
+
if "circular import" in import_msg.lower() or "cannot import name" in import_msg.lower():
|
980
|
+
raise OrionisContainerException(
|
981
|
+
f"Circular import detected while resolving dependencies for '{target.__name__}' in module '{module_name}'.\n"
|
982
|
+
f"This typically happens when two modules import each other. Consider:\n"
|
983
|
+
f"1. Restructuring your code to avoid circular dependencies\n"
|
984
|
+
f"2. Using delayed imports inside methods rather than at module level\n"
|
985
|
+
f"3. Using dependency injection to break the cycle\n"
|
986
|
+
f"Original error: {import_msg}"
|
987
|
+
) from e
|
988
|
+
else:
|
989
|
+
raise OrionisContainerException(
|
990
|
+
f"Import error while resolving dependencies for '{target.__name__}' in module '{module_name}':\n"
|
991
|
+
f"{import_msg}"
|
992
|
+
) from e
|
993
|
+
|
994
|
+
except Exception as e:
|
995
|
+
|
996
|
+
# Get more context about where the error occurred
|
997
|
+
target_type = "class" if isinstance(target, type) else "function"
|
998
|
+
target_name = target.__name__ if hasattr(target, '__name__') else str(target)
|
999
|
+
module_name = target.__module__ if hasattr(target, '__module__') else "unknown module"
|
1000
|
+
|
1001
|
+
raise OrionisContainerException(
|
1002
|
+
f"Error resolving dependencies for {target_type} '{target_name}' in '{module_name}':\n"
|
1003
|
+
f"{str(e)}\n"
|
1004
|
+
f"Check that all dependencies are properly registered in the container."
|
1005
|
+
) from e
|