orionis 0.449.0__py3-none-any.whl → 0.451.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/console/base/contracts/command.py +0 -2
- orionis/console/core/__init__.py +0 -0
- orionis/container/container.py +1620 -88
- orionis/container/context/scope.py +1 -1
- orionis/container/contracts/container.py +184 -33
- orionis/foundation/config/database/entities/mysql.py +0 -1
- orionis/metadata/framework.py +1 -1
- orionis/services/inspirational/contracts/__init__.py +0 -0
- orionis/services/introspection/abstract/contracts/reflection.py +5 -6
- orionis/services/introspection/abstract/reflection.py +5 -6
- orionis/services/introspection/callables/contracts/reflection.py +3 -2
- orionis/services/introspection/callables/reflection.py +4 -4
- orionis/services/introspection/concretes/contracts/reflection.py +5 -6
- orionis/services/introspection/concretes/reflection.py +5 -6
- orionis/services/introspection/dependencies/contracts/reflection.py +87 -23
- orionis/services/introspection/dependencies/entities/argument.py +95 -0
- orionis/services/introspection/dependencies/entities/resolve_argument.py +82 -0
- orionis/services/introspection/dependencies/reflection.py +176 -106
- orionis/services/introspection/instances/contracts/reflection.py +5 -6
- orionis/services/introspection/instances/reflection.py +5 -6
- orionis/test/core/unit_test.py +150 -48
- {orionis-0.449.0.dist-info → orionis-0.451.0.dist-info}/METADATA +1 -1
- {orionis-0.449.0.dist-info → orionis-0.451.0.dist-info}/RECORD +35 -38
- tests/container/mocks/mock_auto_resolution.py +192 -0
- tests/services/introspection/dependencies/test_reflect_dependencies.py +135 -58
- tests/services/introspection/reflection/test_reflection_abstract.py +5 -4
- tests/services/introspection/reflection/test_reflection_callable.py +3 -3
- tests/services/introspection/reflection/test_reflection_concrete.py +4 -4
- tests/services/introspection/reflection/test_reflection_instance.py +5 -5
- orionis/console/args/parser.py +0 -40
- orionis/container/contracts/resolver.py +0 -115
- orionis/container/resolver/resolver.py +0 -602
- orionis/services/introspection/dependencies/entities/callable_dependencies.py +0 -54
- orionis/services/introspection/dependencies/entities/class_dependencies.py +0 -61
- orionis/services/introspection/dependencies/entities/known_dependencies.py +0 -67
- orionis/services/introspection/dependencies/entities/method_dependencies.py +0 -61
- tests/container/resolver/test_resolver.py +0 -62
- /orionis/{container/resolver → console/commands}/__init__.py +0 -0
- {tests/container/resolver → orionis/console/contracts}/__init__.py +0 -0
- {orionis-0.449.0.dist-info → orionis-0.451.0.dist-info}/WHEEL +0 -0
- {orionis-0.449.0.dist-info → orionis-0.451.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.449.0.dist-info → orionis-0.451.0.dist-info}/top_level.txt +0 -0
- {orionis-0.449.0.dist-info → orionis-0.451.0.dist-info}/zip-safe +0 -0
orionis/container/container.py
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import inspect
|
|
1
3
|
import threading
|
|
2
|
-
|
|
4
|
+
import typing
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
3
7
|
from orionis.container.context.manager import ScopeManager
|
|
8
|
+
from orionis.container.context.scope import ScopedContext
|
|
4
9
|
from orionis.container.contracts.container import IContainer
|
|
5
10
|
from orionis.container.entities.binding import Binding
|
|
6
11
|
from orionis.container.enums.lifetimes import Lifetime
|
|
7
12
|
from orionis.container.exceptions import OrionisContainerException
|
|
8
|
-
from orionis.container.
|
|
9
|
-
from orionis.container.validators import (
|
|
10
|
-
ImplementsAbstractMethods,
|
|
11
|
-
IsAbstractClass,
|
|
12
|
-
IsCallable,
|
|
13
|
-
IsConcreteClass,
|
|
14
|
-
IsInstance,
|
|
15
|
-
IsNotSubclass,
|
|
16
|
-
IsSubclass,
|
|
17
|
-
IsValidAlias,
|
|
18
|
-
LifetimeValidator
|
|
19
|
-
)
|
|
13
|
+
from orionis.container.validators import *
|
|
20
14
|
from orionis.services.introspection.abstract.reflection import ReflectionAbstract
|
|
21
15
|
from orionis.services.introspection.callables.reflection import ReflectionCallable
|
|
16
|
+
from orionis.services.introspection.concretes.reflection import ReflectionConcrete
|
|
17
|
+
from orionis.services.introspection.dependencies.entities.argument import Argument
|
|
18
|
+
from orionis.services.introspection.dependencies.entities.resolve_argument import ResolveArguments
|
|
22
19
|
|
|
23
20
|
class Container(IContainer):
|
|
24
21
|
|
|
@@ -71,6 +68,7 @@ class Container(IContainer):
|
|
|
71
68
|
# This write is protected by the lock, ensuring memory visibility
|
|
72
69
|
cls._instances[cls] = instance
|
|
73
70
|
|
|
71
|
+
# Return the newly created instance
|
|
74
72
|
return instance
|
|
75
73
|
|
|
76
74
|
def __init__(self) -> None:
|
|
@@ -84,7 +82,7 @@ class Container(IContainer):
|
|
|
84
82
|
Notes
|
|
85
83
|
-----
|
|
86
84
|
- The `__bindings` dictionary is used to store service bindings.
|
|
87
|
-
- The `
|
|
85
|
+
- The `__aliases` dictionary is used to store service aliases.
|
|
88
86
|
- Initialization occurs only once per instance, regardless of how many times __init__ is called.
|
|
89
87
|
- The container registers itself under the IContainer interface to allow for dependency injection.
|
|
90
88
|
"""
|
|
@@ -92,9 +90,18 @@ class Container(IContainer):
|
|
|
92
90
|
# Check if the instance has already been initialized
|
|
93
91
|
if not hasattr(self, '_Container__initialized'):
|
|
94
92
|
|
|
93
|
+
# Current Project Namespaces
|
|
94
|
+
# Get current project namespace from the working directory
|
|
95
|
+
current_project_namespace = Path().cwd().name
|
|
96
|
+
|
|
97
|
+
# Add the current project namespace to valid namespaces
|
|
98
|
+
self.__valid_namespaces = set(('app', 'orionis', 'requests', 'rich', 'apscheduler', 'dotenv', current_project_namespace))
|
|
99
|
+
|
|
95
100
|
# Initialize the container's internal state
|
|
96
101
|
self.__bindings = {}
|
|
97
|
-
self.
|
|
102
|
+
self.__aliases = {}
|
|
103
|
+
self.__resolution_cache = {}
|
|
104
|
+
self.__singleton_cache = {}
|
|
98
105
|
|
|
99
106
|
# Mark this instance as initialized
|
|
100
107
|
self.__initialized = True
|
|
@@ -106,7 +113,7 @@ class Container(IContainer):
|
|
|
106
113
|
*,
|
|
107
114
|
alias: str = None,
|
|
108
115
|
enforce_decoupling: bool = False
|
|
109
|
-
) -> bool:
|
|
116
|
+
) -> Optional[bool]:
|
|
110
117
|
"""
|
|
111
118
|
Registers a service with a transient lifetime.
|
|
112
119
|
|
|
@@ -180,7 +187,7 @@ class Container(IContainer):
|
|
|
180
187
|
)
|
|
181
188
|
|
|
182
189
|
# Register the alias
|
|
183
|
-
self.
|
|
190
|
+
self.__aliases[alias] = self.__bindings[abstract]
|
|
184
191
|
|
|
185
192
|
# Return True to indicate successful registration
|
|
186
193
|
return True
|
|
@@ -192,7 +199,7 @@ class Container(IContainer):
|
|
|
192
199
|
*,
|
|
193
200
|
alias: str = None,
|
|
194
201
|
enforce_decoupling: bool = False
|
|
195
|
-
) -> bool:
|
|
202
|
+
) -> Optional[bool]:
|
|
196
203
|
"""
|
|
197
204
|
Registers a service with a singleton lifetime.
|
|
198
205
|
|
|
@@ -263,7 +270,7 @@ class Container(IContainer):
|
|
|
263
270
|
)
|
|
264
271
|
|
|
265
272
|
# Register the alias
|
|
266
|
-
self.
|
|
273
|
+
self.__aliases[alias] = self.__bindings[abstract]
|
|
267
274
|
|
|
268
275
|
# Return True to indicate successful registration
|
|
269
276
|
return True
|
|
@@ -275,7 +282,7 @@ class Container(IContainer):
|
|
|
275
282
|
*,
|
|
276
283
|
alias: str = None,
|
|
277
284
|
enforce_decoupling: bool = False
|
|
278
|
-
) -> bool:
|
|
285
|
+
) -> Optional[bool]:
|
|
279
286
|
"""
|
|
280
287
|
Registers a service with a scoped lifetime.
|
|
281
288
|
|
|
@@ -346,7 +353,7 @@ class Container(IContainer):
|
|
|
346
353
|
)
|
|
347
354
|
|
|
348
355
|
# Register the alias
|
|
349
|
-
self.
|
|
356
|
+
self.__aliases[alias] = self.__bindings[abstract]
|
|
350
357
|
|
|
351
358
|
# Return True to indicate successful registration
|
|
352
359
|
return True
|
|
@@ -358,7 +365,7 @@ class Container(IContainer):
|
|
|
358
365
|
*,
|
|
359
366
|
alias: str = None,
|
|
360
367
|
enforce_decoupling: bool = False
|
|
361
|
-
) -> bool:
|
|
368
|
+
) -> Optional[bool]:
|
|
362
369
|
"""
|
|
363
370
|
Registers an instance of a class or interface in the container.
|
|
364
371
|
Parameters
|
|
@@ -427,7 +434,7 @@ class Container(IContainer):
|
|
|
427
434
|
)
|
|
428
435
|
|
|
429
436
|
# Register the alias
|
|
430
|
-
self.
|
|
437
|
+
self.__aliases[alias] = self.__bindings[abstract]
|
|
431
438
|
|
|
432
439
|
# Return True to indicate successful registration
|
|
433
440
|
return True
|
|
@@ -438,7 +445,7 @@ class Container(IContainer):
|
|
|
438
445
|
fn: Callable[..., Any],
|
|
439
446
|
*,
|
|
440
447
|
lifetime: Lifetime = Lifetime.TRANSIENT
|
|
441
|
-
) -> bool:
|
|
448
|
+
) -> Optional[bool]:
|
|
442
449
|
"""
|
|
443
450
|
Registers a function or factory under a given alias.
|
|
444
451
|
|
|
@@ -474,7 +481,7 @@ class Container(IContainer):
|
|
|
474
481
|
IsCallable(fn)
|
|
475
482
|
|
|
476
483
|
# Inspect the function signature
|
|
477
|
-
params = ReflectionCallable(fn).getDependencies()
|
|
484
|
+
params: ResolveArguments = ReflectionCallable(fn).getDependencies()
|
|
478
485
|
|
|
479
486
|
# If the function requires arguments, only allow TRANSIENT
|
|
480
487
|
if (len(params.resolved) + len(params.unresolved)) > 0 and lifetime != Lifetime.TRANSIENT:
|
|
@@ -493,7 +500,7 @@ class Container(IContainer):
|
|
|
493
500
|
)
|
|
494
501
|
|
|
495
502
|
# Register the function as a binding
|
|
496
|
-
self.
|
|
503
|
+
self.__aliases[alias] = self.__bindings[alias]
|
|
497
504
|
|
|
498
505
|
return True
|
|
499
506
|
|
|
@@ -512,149 +519,1674 @@ class Container(IContainer):
|
|
|
512
519
|
Returns
|
|
513
520
|
-------
|
|
514
521
|
bool
|
|
515
|
-
True if the service is registered
|
|
522
|
+
True if the service is registered and has a valid binding, False otherwise.
|
|
516
523
|
|
|
517
524
|
Notes
|
|
518
525
|
-----
|
|
519
|
-
This method
|
|
520
|
-
|
|
526
|
+
This method not only checks for the existence of a binding but also validates
|
|
527
|
+
its integrity. If a binding exists but is corrupted, this method will return
|
|
528
|
+
False to prevent using invalid bindings.
|
|
521
529
|
"""
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
# Use getBinding which includes validation
|
|
533
|
+
binding = self.getBinding(abstract_or_alias)
|
|
534
|
+
return binding is not None
|
|
535
|
+
except OrionisContainerException:
|
|
536
|
+
# If binding validation fails, consider it as not bound
|
|
537
|
+
return False
|
|
526
538
|
|
|
527
539
|
def getBinding(
|
|
528
540
|
self,
|
|
529
541
|
abstract_or_alias: Any
|
|
530
|
-
) -> Binding:
|
|
542
|
+
) -> Optional[Binding]:
|
|
531
543
|
"""
|
|
532
544
|
Retrieves the binding for the requested abstract type or alias.
|
|
533
545
|
|
|
546
|
+
This method performs a lookup in the container's internal binding dictionaries
|
|
547
|
+
to find the binding associated with the provided abstract type or alias.
|
|
548
|
+
It first searches in the primary bindings dictionary, then in the aliases
|
|
549
|
+
dictionary if no direct binding is found.
|
|
550
|
+
|
|
534
551
|
Parameters
|
|
535
552
|
----------
|
|
536
553
|
abstract_or_alias : Any
|
|
537
|
-
The abstract class, interface, or alias (str) to retrieve.
|
|
554
|
+
The abstract class, interface, or alias (str) to retrieve the binding for.
|
|
555
|
+
This can be either a class type or a string alias that was registered
|
|
556
|
+
with the container.
|
|
538
557
|
|
|
539
558
|
Returns
|
|
540
559
|
-------
|
|
541
|
-
Binding
|
|
542
|
-
The binding associated with the requested abstract type or alias.
|
|
560
|
+
Binding or None
|
|
561
|
+
The binding object associated with the requested abstract type or alias.
|
|
562
|
+
Returns None if no binding is found for the provided abstract_or_alias.
|
|
563
|
+
The binding contains information about the service registration including
|
|
564
|
+
the concrete implementation, lifetime, and other configuration details.
|
|
565
|
+
|
|
566
|
+
Raises
|
|
567
|
+
------
|
|
568
|
+
OrionisContainerException
|
|
569
|
+
If a binding is found but is corrupted or incomplete.
|
|
570
|
+
|
|
571
|
+
Notes
|
|
572
|
+
-----
|
|
573
|
+
The method searches in the following order:
|
|
574
|
+
1. Direct lookup in the bindings dictionary using the abstract type
|
|
575
|
+
2. Lookup in the aliases dictionary using the provided alias
|
|
576
|
+
|
|
577
|
+
This method validates binding integrity before returning it to ensure
|
|
578
|
+
that only valid, complete bindings are returned to the caller.
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
# First, attempt to find the binding directly using the abstract type
|
|
582
|
+
# This handles cases where the service was registered with its abstract class
|
|
583
|
+
binding = self.__bindings.get(abstract_or_alias)
|
|
584
|
+
if binding:
|
|
585
|
+
self.__validateBinding(binding, abstract_or_alias)
|
|
586
|
+
return binding
|
|
587
|
+
|
|
588
|
+
# If no direct binding found, search in the aliases dictionary
|
|
589
|
+
# This handles cases where the service is being requested by its alias
|
|
590
|
+
binding = self.__aliases.get(abstract_or_alias)
|
|
591
|
+
if binding:
|
|
592
|
+
self.__validateBinding(binding, abstract_or_alias)
|
|
593
|
+
return binding
|
|
594
|
+
|
|
595
|
+
# Return None if no binding is found for the requested abstract type or alias
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
def __validateBinding(
|
|
599
|
+
self,
|
|
600
|
+
binding: Binding,
|
|
601
|
+
requested_key: Any
|
|
602
|
+
) -> None:
|
|
603
|
+
"""
|
|
604
|
+
Validates the integrity and completeness of a binding.
|
|
605
|
+
|
|
606
|
+
This method ensures that the binding contains all necessary information
|
|
607
|
+
for successful resolution and that the binding data is consistent and
|
|
608
|
+
not corrupted.
|
|
609
|
+
|
|
610
|
+
Parameters
|
|
611
|
+
----------
|
|
612
|
+
binding : Binding
|
|
613
|
+
The binding to validate.
|
|
614
|
+
requested_key : Any
|
|
615
|
+
The key (abstract type or alias) that was used to retrieve this binding.
|
|
616
|
+
Used for error reporting context.
|
|
617
|
+
|
|
618
|
+
Raises
|
|
619
|
+
------
|
|
620
|
+
OrionisContainerException
|
|
621
|
+
If the binding is invalid, incomplete, or corrupted.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
# Ensure the binding is actually a Binding instance
|
|
625
|
+
if not isinstance(binding, Binding):
|
|
626
|
+
raise OrionisContainerException(
|
|
627
|
+
f"Corrupted binding found for '{requested_key}': expected Binding instance, "
|
|
628
|
+
f"got {type(binding).__name__}"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# Ensure the binding has a valid lifetime
|
|
632
|
+
if not hasattr(binding, 'lifetime') or binding.lifetime is None:
|
|
633
|
+
raise OrionisContainerException(
|
|
634
|
+
f"Invalid binding for '{requested_key}': missing or null lifetime"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Validate that the lifetime is a valid Lifetime enum value
|
|
638
|
+
if not isinstance(binding.lifetime, Lifetime):
|
|
639
|
+
raise OrionisContainerException(
|
|
640
|
+
f"Invalid binding for '{requested_key}': lifetime must be a Lifetime enum value, "
|
|
641
|
+
f"got {type(binding.lifetime).__name__}"
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Ensure the binding has at least one resolution method
|
|
645
|
+
has_concrete = hasattr(binding, 'concrete') and binding.concrete is not None
|
|
646
|
+
has_instance = hasattr(binding, 'instance') and binding.instance is not None
|
|
647
|
+
has_function = hasattr(binding, 'function') and binding.function is not None
|
|
648
|
+
|
|
649
|
+
if not (has_concrete or has_instance or has_function):
|
|
650
|
+
raise OrionisContainerException(
|
|
651
|
+
f"Incomplete binding for '{requested_key}': binding must have at least one of "
|
|
652
|
+
"concrete class, instance, or function defined"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Validate that only one primary resolution method is defined
|
|
656
|
+
resolution_methods = [has_concrete, has_instance, has_function]
|
|
657
|
+
if sum(resolution_methods) > 1:
|
|
658
|
+
methods = []
|
|
659
|
+
if has_concrete:
|
|
660
|
+
methods.append("concrete")
|
|
661
|
+
if has_instance:
|
|
662
|
+
methods.append("instance")
|
|
663
|
+
if has_function:
|
|
664
|
+
methods.append("function")
|
|
665
|
+
|
|
666
|
+
raise OrionisContainerException(
|
|
667
|
+
f"Conflicting binding for '{requested_key}': binding has multiple resolution methods "
|
|
668
|
+
f"defined: {', '.join(methods)}. Only one should be specified."
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Additional validations based on binding type
|
|
672
|
+
self.__validateBindingByType(binding, requested_key)
|
|
673
|
+
|
|
674
|
+
def __validateBindingByType(
|
|
675
|
+
self,
|
|
676
|
+
binding: Binding,
|
|
677
|
+
requested_key: Any
|
|
678
|
+
) -> None:
|
|
679
|
+
"""
|
|
680
|
+
Performs type-specific validation for different binding types.
|
|
681
|
+
|
|
682
|
+
Parameters
|
|
683
|
+
----------
|
|
684
|
+
binding : Binding
|
|
685
|
+
The binding to validate.
|
|
686
|
+
requested_key : Any
|
|
687
|
+
The key used to retrieve this binding (for error context).
|
|
688
|
+
|
|
689
|
+
Raises
|
|
690
|
+
------
|
|
691
|
+
OrionisContainerException
|
|
692
|
+
If type-specific validation fails.
|
|
543
693
|
"""
|
|
544
|
-
|
|
694
|
+
|
|
695
|
+
# Validate concrete class bindings
|
|
696
|
+
if hasattr(binding, 'concrete') and binding.concrete is not None:
|
|
697
|
+
if not callable(binding.concrete):
|
|
698
|
+
raise OrionisContainerException(
|
|
699
|
+
f"Invalid concrete binding for '{requested_key}': concrete must be callable"
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if not isinstance(binding.concrete, type):
|
|
703
|
+
raise OrionisContainerException(
|
|
704
|
+
f"Invalid concrete binding for '{requested_key}': concrete must be a class type"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# Validate instance bindings
|
|
708
|
+
elif hasattr(binding, 'instance') and binding.instance is not None:
|
|
709
|
+
# Instance bindings should always be singletons
|
|
710
|
+
if binding.lifetime != Lifetime.SINGLETON:
|
|
711
|
+
raise OrionisContainerException(
|
|
712
|
+
f"Invalid instance binding for '{requested_key}': instances must have "
|
|
713
|
+
f"SINGLETON lifetime, got {binding.lifetime.name}"
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Validate function bindings
|
|
717
|
+
elif hasattr(binding, 'function') and binding.function is not None:
|
|
718
|
+
if not callable(binding.function):
|
|
719
|
+
raise OrionisContainerException(
|
|
720
|
+
f"Invalid function binding for '{requested_key}': function must be callable"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Validate contract and alias consistency
|
|
724
|
+
if hasattr(binding, 'contract') and binding.contract is not None:
|
|
725
|
+
if not isinstance(binding.contract, type):
|
|
726
|
+
raise OrionisContainerException(
|
|
727
|
+
f"Invalid contract in binding for '{requested_key}': contract must be a class type"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
if hasattr(binding, 'alias') and binding.alias is not None:
|
|
731
|
+
if not isinstance(binding.alias, str):
|
|
732
|
+
raise OrionisContainerException(
|
|
733
|
+
f"Invalid alias in binding for '{requested_key}': alias must be a string"
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
if len(binding.alias.strip()) == 0:
|
|
737
|
+
raise OrionisContainerException(
|
|
738
|
+
f"Invalid alias in binding for '{requested_key}': alias cannot be empty"
|
|
739
|
+
)
|
|
545
740
|
|
|
546
741
|
def drop(
|
|
547
742
|
self,
|
|
548
743
|
abstract: Callable[..., Any] = None,
|
|
549
744
|
alias: str = None
|
|
550
|
-
) ->
|
|
745
|
+
) -> bool:
|
|
551
746
|
"""
|
|
552
|
-
|
|
553
|
-
This method allows removing registered services from the dependency injection container,
|
|
554
|
-
either by their abstract type or by their alias. When a service is dropped,
|
|
555
|
-
all its bindings and aliases are removed from the container.
|
|
747
|
+
Removes a service registration from the container by abstract type or alias.
|
|
556
748
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
749
|
+
This method unregisters services from the dependency injection container by removing
|
|
750
|
+
their bindings and associated aliases. When a service is removed, all references to it
|
|
751
|
+
are cleaned up from the container's internal dictionaries, including singleton cache
|
|
752
|
+
and resolution cache.
|
|
753
|
+
|
|
754
|
+
Parameters
|
|
755
|
+
----------
|
|
562
756
|
abstract : Callable[..., Any], optional
|
|
563
|
-
The abstract
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
757
|
+
The abstract class or interface whose registration should be removed.
|
|
758
|
+
When provided, removes the binding for this type and its default alias
|
|
759
|
+
(generated from module and class name).
|
|
760
|
+
alias : str, optional
|
|
761
|
+
The custom alias whose registration should be removed.
|
|
762
|
+
When provided, removes both the alias entry and any associated binding
|
|
763
|
+
registered under this alias name.
|
|
764
|
+
|
|
765
|
+
Returns
|
|
766
|
+
-------
|
|
767
|
+
bool
|
|
768
|
+
True if at least one registration was successfully removed, False if
|
|
769
|
+
no matching registrations were found to remove.
|
|
770
|
+
|
|
771
|
+
Warnings
|
|
772
|
+
--------
|
|
773
|
+
Use this method with caution. Removing essential services can break dependency
|
|
774
|
+
resolution chains and cause runtime failures throughout the application.
|
|
775
|
+
Only remove services when you are certain they are no longer needed.
|
|
567
776
|
|
|
568
777
|
Notes
|
|
569
778
|
-----
|
|
570
|
-
At least one parameter (abstract or alias)
|
|
571
|
-
If both are provided, both will be
|
|
779
|
+
- At least one parameter (abstract or alias) should be provided for meaningful operation
|
|
780
|
+
- If both parameters are provided, both removal operations will be attempted
|
|
781
|
+
- The method handles cases where the specified abstract type or alias doesn't exist
|
|
782
|
+
- Default aliases are automatically generated using the format: 'module.ClassName'
|
|
783
|
+
- All cache entries (singleton and resolution) are cleaned up
|
|
572
784
|
"""
|
|
573
785
|
|
|
786
|
+
deleted = False
|
|
787
|
+
|
|
574
788
|
# If abstract is provided
|
|
575
789
|
if abstract:
|
|
576
790
|
|
|
577
791
|
# Remove the abstract service from the bindings if it exists
|
|
578
792
|
if abstract in self.__bindings:
|
|
579
793
|
del self.__bindings[abstract]
|
|
794
|
+
deleted = True
|
|
580
795
|
|
|
581
796
|
# Remove the default alias (module + class name) from aliases if it exists
|
|
582
797
|
abs_alias = ReflectionAbstract(abstract).getModuleWithClassName()
|
|
583
|
-
if abs_alias in self.
|
|
584
|
-
del self.
|
|
798
|
+
if abs_alias in self.__aliases:
|
|
799
|
+
del self.__aliases[abs_alias]
|
|
800
|
+
deleted = True
|
|
801
|
+
|
|
802
|
+
# Clean up singleton cache for this abstract
|
|
803
|
+
if abstract in self.__singleton_cache:
|
|
804
|
+
del self.__singleton_cache[abstract]
|
|
805
|
+
deleted = True
|
|
806
|
+
|
|
807
|
+
# Clean up resolution cache for this abstract
|
|
808
|
+
if hasattr(abstract, '__module__') and hasattr(abstract, '__name__'):
|
|
809
|
+
type_key = f"{abstract.__module__}.{abstract.__name__}"
|
|
810
|
+
if type_key in self.__resolution_cache:
|
|
811
|
+
del self.__resolution_cache[type_key]
|
|
812
|
+
deleted = True
|
|
585
813
|
|
|
586
814
|
# If a custom alias is provided
|
|
587
815
|
if alias:
|
|
588
816
|
|
|
589
817
|
# Remove it from the aliases dictionary if it exists
|
|
590
|
-
if alias in self.
|
|
591
|
-
del self.
|
|
818
|
+
if alias in self.__aliases:
|
|
819
|
+
del self.__aliases[alias]
|
|
820
|
+
deleted = True
|
|
592
821
|
|
|
593
822
|
# Remove the binding associated with the alias
|
|
594
823
|
if alias in self.__bindings:
|
|
595
824
|
del self.__bindings[alias]
|
|
825
|
+
deleted = True
|
|
826
|
+
|
|
827
|
+
# Clean up singleton cache for this alias
|
|
828
|
+
if alias in self.__singleton_cache:
|
|
829
|
+
del self.__singleton_cache[alias]
|
|
830
|
+
deleted = True
|
|
831
|
+
|
|
832
|
+
# Return if any deletion occurred
|
|
833
|
+
return deleted
|
|
834
|
+
|
|
835
|
+
def createContext(
|
|
836
|
+
self
|
|
837
|
+
) -> ScopeManager:
|
|
838
|
+
"""
|
|
839
|
+
Creates a new context for managing scoped services.
|
|
840
|
+
|
|
841
|
+
This method returns a context manager that can be used with a 'with' statement
|
|
842
|
+
to control the lifecycle of scoped services.
|
|
843
|
+
|
|
844
|
+
Returns
|
|
845
|
+
-------
|
|
846
|
+
ScopeManager
|
|
847
|
+
A context manager for scoped services.
|
|
848
|
+
|
|
849
|
+
Usage
|
|
850
|
+
-------
|
|
851
|
+
with container.createContext():
|
|
852
|
+
# Scoped services created here will be disposed when exiting this block
|
|
853
|
+
service = container.make(IScopedService)
|
|
854
|
+
...
|
|
855
|
+
# Scoped services are automatically disposed here
|
|
856
|
+
"""
|
|
857
|
+
|
|
858
|
+
return ScopeManager()
|
|
596
859
|
|
|
597
860
|
def make(
|
|
598
861
|
self,
|
|
599
|
-
|
|
862
|
+
type_: Any,
|
|
600
863
|
*args: tuple,
|
|
601
864
|
**kwargs: dict
|
|
602
865
|
) -> Any:
|
|
603
866
|
"""
|
|
604
|
-
|
|
867
|
+
Resolve and instantiate a service or type.
|
|
868
|
+
|
|
869
|
+
This method attempts to resolve and instantiate the requested service or type.
|
|
870
|
+
It first checks if the type is registered in the container and, if so, resolves
|
|
871
|
+
it according to its binding and lifetime. If the type is not registered but is
|
|
872
|
+
a class, it attempts to auto-resolve it by constructing it and resolving its
|
|
873
|
+
dependencies recursively. If neither approach is possible, an exception is raised.
|
|
605
874
|
|
|
606
875
|
Parameters
|
|
607
876
|
----------
|
|
608
|
-
|
|
609
|
-
The abstract
|
|
877
|
+
type_ : Any
|
|
878
|
+
The abstract type, class, or alias to resolve. This can be a class, interface,
|
|
879
|
+
or a string alias registered in the container.
|
|
880
|
+
*args : tuple
|
|
881
|
+
Positional arguments to pass to the constructor or factory function.
|
|
882
|
+
**kwargs : dict
|
|
883
|
+
Keyword arguments to pass to the constructor or factory function.
|
|
884
|
+
|
|
885
|
+
Returns
|
|
886
|
+
-------
|
|
887
|
+
Any
|
|
888
|
+
The resolved and instantiated service or object. If the type is registered,
|
|
889
|
+
the instance is created according to its binding's lifetime (singleton,
|
|
890
|
+
transient, or scoped). If the type is not registered but is a class,
|
|
891
|
+
a new instance is created with its dependencies resolved automatically.
|
|
892
|
+
|
|
893
|
+
Raises
|
|
894
|
+
------
|
|
895
|
+
OrionisContainerException
|
|
896
|
+
If the requested service or type is not registered in the container and
|
|
897
|
+
cannot be auto-resolved.
|
|
898
|
+
|
|
899
|
+
Notes
|
|
900
|
+
-----
|
|
901
|
+
- If the type is registered, the container's binding and lifetime rules are used.
|
|
902
|
+
- If the type is not registered but is a class, auto-resolution is attempted.
|
|
903
|
+
- If the type cannot be resolved by either method, an exception is raised.
|
|
904
|
+
"""
|
|
905
|
+
|
|
906
|
+
# Try to resolve from registered bindings first
|
|
907
|
+
if self.bound(type_):
|
|
908
|
+
# Resolve the binding according to its lifetime and configuration
|
|
909
|
+
return self.resolve(
|
|
910
|
+
self.getBinding(type_),
|
|
911
|
+
*args,
|
|
912
|
+
**kwargs
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
# If not registered, try auto-resolution for classes
|
|
916
|
+
if isinstance(type_, type):
|
|
917
|
+
# Attempt to construct the class and resolve its dependencies recursively
|
|
918
|
+
return self.resolveWithoutContainer(
|
|
919
|
+
type_,
|
|
920
|
+
*args,
|
|
921
|
+
**kwargs
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# If all attempts fail, raise an exception indicating resolution failure
|
|
925
|
+
raise OrionisContainerException(
|
|
926
|
+
f"The requested service '{type_}' is not registered in the container "
|
|
927
|
+
f"and cannot be auto-resolved."
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
def resolve(
|
|
931
|
+
self,
|
|
932
|
+
binding: Binding,
|
|
933
|
+
*args,
|
|
934
|
+
**kwargs
|
|
935
|
+
):
|
|
936
|
+
"""
|
|
937
|
+
Resolves an instance from a binding according to its lifetime.
|
|
938
|
+
|
|
939
|
+
Parameters
|
|
940
|
+
----------
|
|
941
|
+
binding : Binding
|
|
942
|
+
The binding to resolve.
|
|
610
943
|
*args : tuple
|
|
611
|
-
|
|
944
|
+
Additional positional arguments to pass to the constructor.
|
|
612
945
|
**kwargs : dict
|
|
613
|
-
|
|
946
|
+
Additional keyword arguments to pass to the constructor.
|
|
614
947
|
|
|
615
948
|
Returns
|
|
616
949
|
-------
|
|
617
950
|
Any
|
|
618
|
-
|
|
951
|
+
The resolved instance.
|
|
619
952
|
|
|
620
953
|
Raises
|
|
621
954
|
------
|
|
622
955
|
OrionisContainerException
|
|
623
|
-
If the
|
|
956
|
+
If the binding is not an instance of Binding or if the lifetime is not supported.
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
# Ensure the binding is an instance of Binding
|
|
960
|
+
if not isinstance(binding, Binding):
|
|
961
|
+
raise OrionisContainerException(
|
|
962
|
+
f"Expected a Binding instance, got {type(binding).__name__}"
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
# Handle based on binding type and lifetime
|
|
966
|
+
if binding.lifetime == Lifetime.TRANSIENT:
|
|
967
|
+
return self.__resolveTransient(binding, *args, **kwargs)
|
|
968
|
+
elif binding.lifetime == Lifetime.SINGLETON:
|
|
969
|
+
return self.__resolveSingleton(binding, *args, **kwargs)
|
|
970
|
+
elif binding.lifetime == Lifetime.SCOPED:
|
|
971
|
+
return self.__resolveScoped(binding, *args, **kwargs)
|
|
972
|
+
|
|
973
|
+
def __resolveTransient(
|
|
974
|
+
self,
|
|
975
|
+
binding: Binding,
|
|
976
|
+
*args,
|
|
977
|
+
**kwargs
|
|
978
|
+
) -> Any:
|
|
979
|
+
"""
|
|
980
|
+
Resolves a service with transient lifetime.
|
|
981
|
+
|
|
982
|
+
Parameters
|
|
983
|
+
----------
|
|
984
|
+
binding : Binding
|
|
985
|
+
The binding to resolve.
|
|
986
|
+
*args : tuple
|
|
987
|
+
Positional arguments to pass to the constructor.
|
|
988
|
+
**kwargs : dict
|
|
989
|
+
Keyword arguments to pass to the constructor.
|
|
990
|
+
|
|
991
|
+
Returns
|
|
992
|
+
-------
|
|
993
|
+
Any
|
|
994
|
+
A new instance of the requested service.
|
|
995
|
+
"""
|
|
996
|
+
|
|
997
|
+
# Check if the binding has a concrete class or function defined
|
|
998
|
+
if binding.concrete:
|
|
999
|
+
if args or kwargs:
|
|
1000
|
+
return self.__instantiateConcreteWithArgs(binding.concrete, *args, **kwargs)
|
|
1001
|
+
else:
|
|
1002
|
+
return self.__instantiateConcreteReflective(binding.concrete)
|
|
1003
|
+
|
|
1004
|
+
# If the binding has a function defined
|
|
1005
|
+
elif binding.function:
|
|
1006
|
+
if args or kwargs:
|
|
1007
|
+
return self.__instantiateCallableWithArgs(binding.function, *args, **kwargs)
|
|
1008
|
+
else:
|
|
1009
|
+
return self.__instantiateCallableReflective(binding.function)
|
|
1010
|
+
|
|
1011
|
+
# If neither concrete class nor function is defined
|
|
1012
|
+
else:
|
|
1013
|
+
raise OrionisContainerException(
|
|
1014
|
+
"Cannot resolve transient binding: neither a concrete class nor a function is defined."
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
def __resolveSingleton(
|
|
1018
|
+
self,
|
|
1019
|
+
binding: Binding,
|
|
1020
|
+
*args,
|
|
1021
|
+
**kwargs
|
|
1022
|
+
) -> Any:
|
|
1023
|
+
"""
|
|
1024
|
+
Resolves a service with singleton lifetime.
|
|
1025
|
+
|
|
1026
|
+
Parameters
|
|
1027
|
+
----------
|
|
1028
|
+
binding : Binding
|
|
1029
|
+
The binding to resolve.
|
|
1030
|
+
*args : tuple
|
|
1031
|
+
Positional arguments to pass to the constructor (only used if instance doesn't exist yet).
|
|
1032
|
+
**kwargs : dict
|
|
1033
|
+
Keyword arguments to pass to the constructor (only used if instance doesn't exist yet).
|
|
1034
|
+
|
|
1035
|
+
Returns
|
|
1036
|
+
-------
|
|
1037
|
+
Any
|
|
1038
|
+
The singleton instance of the requested service.
|
|
624
1039
|
"""
|
|
625
|
-
|
|
1040
|
+
|
|
1041
|
+
# Get the cache key for this binding (prioritize contract over alias)
|
|
1042
|
+
cache_key = self.__getSingletonCacheKey(binding)
|
|
1043
|
+
|
|
1044
|
+
# Return existing instance if available
|
|
1045
|
+
if cache_key in self.__singleton_cache:
|
|
1046
|
+
return self.__singleton_cache[cache_key]
|
|
1047
|
+
|
|
1048
|
+
# Handle pre-registered instances first
|
|
1049
|
+
if binding.instance is not None:
|
|
1050
|
+
# Store the pre-registered instance in cache and return it
|
|
1051
|
+
self.__singleton_cache[cache_key] = binding.instance
|
|
1052
|
+
return self.__singleton_cache[cache_key]
|
|
1053
|
+
|
|
1054
|
+
# Create instance if needed
|
|
1055
|
+
instance = None
|
|
1056
|
+
if binding.concrete:
|
|
1057
|
+
if args or kwargs:
|
|
1058
|
+
instance = self.__instantiateConcreteWithArgs(
|
|
1059
|
+
binding.concrete,
|
|
1060
|
+
*args,
|
|
1061
|
+
**kwargs
|
|
1062
|
+
)
|
|
1063
|
+
else:
|
|
1064
|
+
instance = self.__instantiateConcreteReflective(
|
|
1065
|
+
binding.concrete
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
# If the binding has a function defined
|
|
1069
|
+
elif binding.function:
|
|
1070
|
+
if args or kwargs:
|
|
1071
|
+
instance = self.__instantiateCallableWithArgs(
|
|
1072
|
+
binding.function,
|
|
1073
|
+
*args,
|
|
1074
|
+
**kwargs
|
|
1075
|
+
)
|
|
1076
|
+
else:
|
|
1077
|
+
instance = self.__instantiateCallableReflective(
|
|
1078
|
+
binding.function
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
# If neither concrete class, function, nor instance is defined
|
|
1082
|
+
else:
|
|
626
1083
|
raise OrionisContainerException(
|
|
627
|
-
|
|
1084
|
+
"Failed to resolve singleton binding: no concrete class, instance, or function is defined for this binding. "
|
|
1085
|
+
"Ensure that the binding was registered correctly with a concrete implementation, instance, or callable."
|
|
628
1086
|
)
|
|
629
1087
|
|
|
630
|
-
#
|
|
631
|
-
|
|
1088
|
+
# Store the instance in the singleton cache using the consistent key
|
|
1089
|
+
self.__singleton_cache[cache_key] = instance
|
|
1090
|
+
|
|
1091
|
+
# Store cross-references to ensure we can find the instance by both contract and alias
|
|
1092
|
+
self.__storeSingletonCrossReferences(binding, instance)
|
|
1093
|
+
|
|
1094
|
+
# Return the newly created singleton instance
|
|
1095
|
+
return instance
|
|
1096
|
+
|
|
1097
|
+
def __getSingletonCacheKey(self, binding: Binding) -> Any:
|
|
1098
|
+
"""
|
|
1099
|
+
Determines the primary cache key for a singleton binding.
|
|
1100
|
+
|
|
1101
|
+
This method establishes a consistent caching strategy by prioritizing
|
|
1102
|
+
the contract over the alias for cache key generation. This ensures
|
|
1103
|
+
that each singleton has a single, predictable cache key.
|
|
1104
|
+
|
|
1105
|
+
Parameters
|
|
1106
|
+
----------
|
|
1107
|
+
binding : Binding
|
|
1108
|
+
The binding for which to determine the cache key.
|
|
1109
|
+
|
|
1110
|
+
Returns
|
|
1111
|
+
-------
|
|
1112
|
+
Any
|
|
1113
|
+
The primary cache key to use for this binding.
|
|
1114
|
+
"""
|
|
1115
|
+
|
|
1116
|
+
# Prioritize contract if available (for class-based bindings)
|
|
1117
|
+
if binding.contract is not None:
|
|
1118
|
+
return binding.contract
|
|
632
1119
|
|
|
633
|
-
#
|
|
634
|
-
|
|
635
|
-
binding
|
|
636
|
-
|
|
637
|
-
|
|
1120
|
+
# Fall back to alias for function-based or alias-only bindings
|
|
1121
|
+
if binding.alias is not None:
|
|
1122
|
+
return binding.alias
|
|
1123
|
+
|
|
1124
|
+
# This should never happen with valid bindings, but provide fallback
|
|
1125
|
+
raise OrionisContainerException(
|
|
1126
|
+
"Cannot determine cache key: binding has neither contract nor alias"
|
|
638
1127
|
)
|
|
639
1128
|
|
|
640
|
-
def
|
|
1129
|
+
def __storeSingletonCrossReferences(self, binding: Binding, instance: Any) -> None:
|
|
641
1130
|
"""
|
|
642
|
-
|
|
1131
|
+
Stores cross-references for singleton instances to ensure discoverability.
|
|
643
1132
|
|
|
644
|
-
This method
|
|
645
|
-
|
|
1133
|
+
This method ensures that singleton instances can be found by both their
|
|
1134
|
+
contract and alias, while maintaining a single source of truth for the
|
|
1135
|
+
actual instance storage.
|
|
1136
|
+
|
|
1137
|
+
Parameters
|
|
1138
|
+
----------
|
|
1139
|
+
binding : Binding
|
|
1140
|
+
The binding containing contract and alias information.
|
|
1141
|
+
instance : Any
|
|
1142
|
+
The singleton instance to store cross-references for.
|
|
1143
|
+
"""
|
|
1144
|
+
|
|
1145
|
+
primary_key = self.__getSingletonCacheKey(binding)
|
|
1146
|
+
|
|
1147
|
+
# Store cross-reference for contract if it's not the primary key
|
|
1148
|
+
if binding.contract is not None and binding.contract != primary_key:
|
|
1149
|
+
self.__singleton_cache[binding.contract] = instance
|
|
1150
|
+
|
|
1151
|
+
# Store cross-reference for alias if it's not the primary key
|
|
1152
|
+
if binding.alias is not None and binding.alias != primary_key:
|
|
1153
|
+
self.__singleton_cache[binding.alias] = instance
|
|
1154
|
+
|
|
1155
|
+
def __resolveScoped(
|
|
1156
|
+
self,
|
|
1157
|
+
binding: Binding,
|
|
1158
|
+
*args,
|
|
1159
|
+
**kwargs
|
|
1160
|
+
) -> Any:
|
|
1161
|
+
"""
|
|
1162
|
+
Resolves a service with scoped lifetime.
|
|
1163
|
+
|
|
1164
|
+
Parameters
|
|
1165
|
+
----------
|
|
1166
|
+
binding : Binding
|
|
1167
|
+
The binding to resolve.
|
|
1168
|
+
*args : tuple
|
|
1169
|
+
Positional arguments to pass to the constructor.
|
|
1170
|
+
**kwargs : dict
|
|
1171
|
+
Keyword arguments to pass to the constructor.
|
|
646
1172
|
|
|
647
1173
|
Returns
|
|
648
1174
|
-------
|
|
649
|
-
|
|
650
|
-
|
|
1175
|
+
Any
|
|
1176
|
+
The scoped instance of the requested service.
|
|
651
1177
|
|
|
652
|
-
|
|
1178
|
+
Raises
|
|
1179
|
+
------
|
|
1180
|
+
OrionisContainerException
|
|
1181
|
+
If no scope is active or service can't be resolved.
|
|
1182
|
+
"""
|
|
1183
|
+
|
|
1184
|
+
# Check if the current scope is active
|
|
1185
|
+
scope = ScopedContext.getCurrentScope()
|
|
1186
|
+
|
|
1187
|
+
# If no active scope is found, raise an exception
|
|
1188
|
+
if scope is None:
|
|
1189
|
+
raise OrionisContainerException(
|
|
1190
|
+
f"No active scope found while resolving scoped service '{binding.alias}'. "
|
|
1191
|
+
f"Use 'with container.createContext():' to create a scope context."
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
# If the binding is already in the current scope, return it
|
|
1195
|
+
if binding.contract in scope:
|
|
1196
|
+
return scope[binding.contract]
|
|
1197
|
+
if binding.alias in scope:
|
|
1198
|
+
return scope[binding.alias]
|
|
1199
|
+
|
|
1200
|
+
# Create a new instance
|
|
1201
|
+
if binding.concrete:
|
|
1202
|
+
if args or kwargs:
|
|
1203
|
+
instance = self.__instantiateConcreteWithArgs(
|
|
1204
|
+
binding.concrete,
|
|
1205
|
+
*args,
|
|
1206
|
+
**kwargs
|
|
1207
|
+
)
|
|
1208
|
+
else:
|
|
1209
|
+
instance = self.__instantiateConcreteReflective(
|
|
1210
|
+
binding.concrete
|
|
1211
|
+
)
|
|
1212
|
+
elif binding.function:
|
|
1213
|
+
if args or kwargs:
|
|
1214
|
+
instance = self.__instantiateCallableWithArgs(
|
|
1215
|
+
binding.function,
|
|
1216
|
+
*args,
|
|
1217
|
+
**kwargs
|
|
1218
|
+
)
|
|
1219
|
+
else:
|
|
1220
|
+
instance = self.__instantiateCallableReflective(
|
|
1221
|
+
binding.function
|
|
1222
|
+
)
|
|
1223
|
+
else:
|
|
1224
|
+
raise OrionisContainerException(
|
|
1225
|
+
f"Cannot resolve scoped binding for '{binding.contract} or {binding.alias}': "
|
|
1226
|
+
"No concrete class, instance, or function is defined for this binding. "
|
|
1227
|
+
"Ensure that the binding was registered correctly with a concrete implementation, instance, or callable."
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
# Store the instance in the current scope and return it
|
|
1231
|
+
scope[binding.contract] = instance
|
|
1232
|
+
|
|
1233
|
+
# Return the newly created instance
|
|
1234
|
+
return scope[binding.contract]
|
|
1235
|
+
|
|
1236
|
+
def __instantiateConcreteWithArgs(
|
|
1237
|
+
self,
|
|
1238
|
+
concrete: Callable[..., Any],
|
|
1239
|
+
*args,
|
|
1240
|
+
**kwargs
|
|
1241
|
+
) -> Any:
|
|
1242
|
+
"""
|
|
1243
|
+
Instantiates a concrete class with the provided arguments.
|
|
1244
|
+
|
|
1245
|
+
Parameters
|
|
1246
|
+
----------
|
|
1247
|
+
concrete : Callable[..., Any]
|
|
1248
|
+
Class to instantiate.
|
|
1249
|
+
*args : tuple
|
|
1250
|
+
Positional arguments to pass to the constructor.
|
|
1251
|
+
**kwargs : dict
|
|
1252
|
+
Keyword arguments to pass to the constructor.
|
|
1253
|
+
|
|
1254
|
+
Returns
|
|
653
1255
|
-------
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
#
|
|
1256
|
+
object
|
|
1257
|
+
A new instance of the specified concrete class.
|
|
1258
|
+
"""
|
|
1259
|
+
|
|
1260
|
+
# try to instantiate the concrete class with the provided arguments
|
|
1261
|
+
try:
|
|
1262
|
+
|
|
1263
|
+
# If the concrete is a class, instantiate it directly
|
|
1264
|
+
return concrete(*args, **kwargs)
|
|
1265
|
+
|
|
1266
|
+
except TypeError as e:
|
|
1267
|
+
|
|
1268
|
+
# If instantiation fails, use ReflectionConcrete to get class name and constructor signature
|
|
1269
|
+
rf_concrete = ReflectionConcrete(concrete)
|
|
1270
|
+
class_name = rf_concrete.getClassName()
|
|
1271
|
+
signature = rf_concrete.getConstructorSignature()
|
|
1272
|
+
|
|
1273
|
+
# Raise an exception with detailed information about the failure
|
|
1274
|
+
raise OrionisContainerException(
|
|
1275
|
+
f"Failed to instantiate [{class_name}] with the provided arguments: {e}\n"
|
|
1276
|
+
f"Expected constructor signature: [{signature}]"
|
|
1277
|
+
) from e
|
|
1278
|
+
|
|
1279
|
+
def __instantiateCallableWithArgs(
|
|
1280
|
+
self,
|
|
1281
|
+
fn: Callable[..., Any],
|
|
1282
|
+
*args,
|
|
1283
|
+
**kwargs
|
|
1284
|
+
) -> Any:
|
|
659
1285
|
"""
|
|
660
|
-
|
|
1286
|
+
Invokes a callable with the provided arguments.
|
|
1287
|
+
|
|
1288
|
+
Parameters
|
|
1289
|
+
----------
|
|
1290
|
+
fn : Callable[..., Any]
|
|
1291
|
+
The callable to invoke.
|
|
1292
|
+
*args : tuple
|
|
1293
|
+
Positional arguments to pass to the callable.
|
|
1294
|
+
**kwargs : dict
|
|
1295
|
+
Keyword arguments to pass to the callable.
|
|
1296
|
+
|
|
1297
|
+
Returns
|
|
1298
|
+
-------
|
|
1299
|
+
Any
|
|
1300
|
+
The result of the callable.
|
|
1301
|
+
"""
|
|
1302
|
+
|
|
1303
|
+
# Try to invoke the callable with the provided arguments
|
|
1304
|
+
try:
|
|
1305
|
+
|
|
1306
|
+
# If the callable is a function, invoke it directly
|
|
1307
|
+
return fn(*args, **kwargs)
|
|
1308
|
+
|
|
1309
|
+
except TypeError as e:
|
|
1310
|
+
|
|
1311
|
+
# If invocation fails, use ReflectionCallable to get function name and signature
|
|
1312
|
+
rf_callable = ReflectionCallable(fn)
|
|
1313
|
+
function_name = rf_callable.getName()
|
|
1314
|
+
signature = rf_callable.getSignature()
|
|
1315
|
+
|
|
1316
|
+
# Raise an exception with detailed information about the failure
|
|
1317
|
+
raise OrionisContainerException(
|
|
1318
|
+
f"Failed to invoke function [{function_name}] with the provided arguments: {e}\n"
|
|
1319
|
+
f"Expected function signature: [{signature}]"
|
|
1320
|
+
) from e
|
|
1321
|
+
|
|
1322
|
+
def __instantiateConcreteReflective(
|
|
1323
|
+
self,
|
|
1324
|
+
concrete: Callable[..., Any]
|
|
1325
|
+
) -> Any:
|
|
1326
|
+
"""
|
|
1327
|
+
Instantiates a concrete class reflectively, resolving its dependencies from the container.
|
|
1328
|
+
|
|
1329
|
+
Parameters
|
|
1330
|
+
----------
|
|
1331
|
+
concrete : Callable[..., Any]
|
|
1332
|
+
The concrete class to instantiate.
|
|
1333
|
+
|
|
1334
|
+
Returns
|
|
1335
|
+
-------
|
|
1336
|
+
Any
|
|
1337
|
+
A new instance of the concrete class.
|
|
1338
|
+
"""
|
|
1339
|
+
|
|
1340
|
+
return self.__instantiateWithReflection(concrete, is_class=True)
|
|
1341
|
+
|
|
1342
|
+
def __instantiateCallableReflective(
|
|
1343
|
+
self,
|
|
1344
|
+
fn: Callable[..., Any]
|
|
1345
|
+
) -> Any:
|
|
1346
|
+
"""
|
|
1347
|
+
Invokes a callable reflectively, resolving its dependencies from the container.
|
|
1348
|
+
|
|
1349
|
+
Parameters
|
|
1350
|
+
----------
|
|
1351
|
+
fn : Callable[..., Any]
|
|
1352
|
+
The callable to invoke.
|
|
1353
|
+
|
|
1354
|
+
Returns
|
|
1355
|
+
-------
|
|
1356
|
+
Any
|
|
1357
|
+
The result of the callable.
|
|
1358
|
+
"""
|
|
1359
|
+
|
|
1360
|
+
# Try simple call first for functions without parameters
|
|
1361
|
+
try:
|
|
1362
|
+
|
|
1363
|
+
# Use ReflectionCallable to get dependencies
|
|
1364
|
+
dependencies = ReflectionCallable(fn).getDependencies()
|
|
1365
|
+
|
|
1366
|
+
# If there are no required parameters, call directly
|
|
1367
|
+
if not dependencies or (not dependencies.resolved and not dependencies.unresolved):
|
|
1368
|
+
return fn()
|
|
1369
|
+
|
|
1370
|
+
# If there are unresolved dependencies, raise an exception
|
|
1371
|
+
if dependencies.unresolved:
|
|
1372
|
+
unresolved_args = [name for name in dependencies.unresolved.keys()]
|
|
1373
|
+
raise OrionisContainerException(
|
|
1374
|
+
f"Cannot invoke callable '{getattr(fn, '__name__', str(fn))}' because the following required arguments are missing: [{', '.join(unresolved_args)}]."
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
# Otherwise, resolve dependencies and call with them
|
|
1378
|
+
resolved_params = self.__resolveDependencies(
|
|
1379
|
+
getattr(fn, '__name__', str(fn)),
|
|
1380
|
+
dependencies
|
|
1381
|
+
)
|
|
1382
|
+
return fn(**resolved_params)
|
|
1383
|
+
|
|
1384
|
+
except Exception as inspect_error:
|
|
1385
|
+
|
|
1386
|
+
# If inspection fails, try direct call as last resort
|
|
1387
|
+
try:
|
|
1388
|
+
return fn()
|
|
1389
|
+
|
|
1390
|
+
# If direct call fails, raise inspection error
|
|
1391
|
+
except TypeError:
|
|
1392
|
+
raise OrionisContainerException(
|
|
1393
|
+
f"Failed to invoke callable: {inspect_error}"
|
|
1394
|
+
) from inspect_error
|
|
1395
|
+
|
|
1396
|
+
def __reflectTarget(
|
|
1397
|
+
self,
|
|
1398
|
+
target: Callable[..., Any],
|
|
1399
|
+
*,
|
|
1400
|
+
is_class: bool = True
|
|
1401
|
+
) -> tuple:
|
|
1402
|
+
"""
|
|
1403
|
+
Analyzes the target (class or callable) and extracts its dependency signature.
|
|
1404
|
+
|
|
1405
|
+
This method uses reflection to inspect either a class or a callable (function/method)
|
|
1406
|
+
and retrieves its constructor or callable dependencies. It determines the appropriate
|
|
1407
|
+
reflection strategy based on the `is_class` flag.
|
|
1408
|
+
|
|
1409
|
+
Parameters
|
|
1410
|
+
----------
|
|
1411
|
+
target : Callable[..., Any]
|
|
1412
|
+
The class or callable to be analyzed for dependencies.
|
|
1413
|
+
is_class : bool, optional
|
|
1414
|
+
If True, treats the target as a class and inspects its constructor.
|
|
1415
|
+
If False, treats the target as a callable and inspects its signature.
|
|
1416
|
+
Default is True.
|
|
1417
|
+
|
|
1418
|
+
Returns
|
|
1419
|
+
-------
|
|
1420
|
+
tuple
|
|
1421
|
+
A tuple containing:
|
|
1422
|
+
- name (str): The name of the class or callable.
|
|
1423
|
+
- dependencies (ResolveArguments): The resolved dependencies for the target.
|
|
1424
|
+
|
|
1425
|
+
Notes
|
|
1426
|
+
-----
|
|
1427
|
+
This method is intended for internal use to facilitate dependency resolution
|
|
1428
|
+
by extracting the required arguments for instantiation or invocation.
|
|
1429
|
+
"""
|
|
1430
|
+
|
|
1431
|
+
# Select the appropriate reflection class based on whether the target is a class or callable
|
|
1432
|
+
reflection = ReflectionConcrete(target) if is_class else ReflectionCallable(target)
|
|
1433
|
+
|
|
1434
|
+
# Get the dependencies for the constructor (if class) or signature (if callable)
|
|
1435
|
+
dependencies = reflection.getConstructorDependencies() if is_class else reflection.getDependencies()
|
|
1436
|
+
|
|
1437
|
+
# Get the name of the class or callable for identification
|
|
1438
|
+
name = reflection.getClassName() if is_class else reflection.getName()
|
|
1439
|
+
|
|
1440
|
+
# Return both the name and the dependencies as a tuple
|
|
1441
|
+
return name, dependencies
|
|
1442
|
+
|
|
1443
|
+
def resolveDependencyArguments(
|
|
1444
|
+
self,
|
|
1445
|
+
name: Optional[str],
|
|
1446
|
+
dependencies: Optional[ResolveArguments]
|
|
1447
|
+
) -> dict:
|
|
1448
|
+
"""
|
|
1449
|
+
Public method to resolve dependencies for a given class or callable.
|
|
1450
|
+
|
|
1451
|
+
This method serves as the public interface for resolving dependencies.
|
|
1452
|
+
It wraps the internal dependency resolution logic and provides error
|
|
1453
|
+
handling to ensure that any exceptions are communicated clearly.
|
|
1454
|
+
|
|
1455
|
+
Parameters
|
|
1456
|
+
----------
|
|
1457
|
+
name : str or None
|
|
1458
|
+
The name of the class or callable whose dependencies are being resolved.
|
|
1459
|
+
Used for error reporting and context.
|
|
1460
|
+
dependencies : ResolveArguments or None
|
|
1461
|
+
The dependencies object containing resolved and unresolved arguments,
|
|
1462
|
+
as extracted by reflection from the target's signature.
|
|
1463
|
+
|
|
1464
|
+
Returns
|
|
1465
|
+
-------
|
|
1466
|
+
dict
|
|
1467
|
+
A dictionary mapping parameter names to their resolved values. Each key
|
|
1468
|
+
is the name of a constructor or callable parameter, and each value is
|
|
1469
|
+
the resolved dependency instance or value.
|
|
1470
|
+
|
|
1471
|
+
Raises
|
|
1472
|
+
------
|
|
1473
|
+
OrionisContainerException
|
|
1474
|
+
If any required dependency cannot be resolved, if there are unresolved
|
|
1475
|
+
arguments, or if a dependency refers to a built-in type.
|
|
1476
|
+
"""
|
|
1477
|
+
|
|
1478
|
+
return self.__resolveDependencies(name, dependencies)
|
|
1479
|
+
|
|
1480
|
+
def __resolveDependencies(
|
|
1481
|
+
self,
|
|
1482
|
+
name: Optional[str],
|
|
1483
|
+
dependencies: Optional[ResolveArguments]
|
|
1484
|
+
) -> dict:
|
|
1485
|
+
"""
|
|
1486
|
+
Resolves and returns a dictionary of dependencies for a given class or callable.
|
|
1487
|
+
|
|
1488
|
+
This method analyzes the provided dependencies (as extracted by reflection)
|
|
1489
|
+
and attempts to resolve each required argument. It checks for unresolved
|
|
1490
|
+
dependencies, handles default values, and recursively resolves dependencies
|
|
1491
|
+
using the container or auto-resolution logic.
|
|
1492
|
+
|
|
1493
|
+
Parameters
|
|
1494
|
+
----------
|
|
1495
|
+
name : str or None
|
|
1496
|
+
The name of the class or callable whose dependencies are being resolved.
|
|
1497
|
+
Used for error reporting and context.
|
|
1498
|
+
dependencies : ResolveArguments or None
|
|
1499
|
+
The dependencies object containing resolved and unresolved arguments,
|
|
1500
|
+
as extracted by reflection from the target's signature.
|
|
1501
|
+
|
|
1502
|
+
Returns
|
|
1503
|
+
-------
|
|
1504
|
+
dict
|
|
1505
|
+
A dictionary mapping parameter names to their resolved values. Each key
|
|
1506
|
+
is the name of a constructor or callable parameter, and each value is
|
|
1507
|
+
the resolved dependency instance or value.
|
|
1508
|
+
|
|
1509
|
+
Raises
|
|
1510
|
+
------
|
|
1511
|
+
OrionisContainerException
|
|
1512
|
+
If any required dependency cannot be resolved, if there are unresolved
|
|
1513
|
+
arguments, or if a dependency refers to a built-in type.
|
|
1514
|
+
"""
|
|
1515
|
+
|
|
1516
|
+
try:
|
|
1517
|
+
# If there are no dependencies, return empty dict
|
|
1518
|
+
if not dependencies:
|
|
1519
|
+
return {}
|
|
1520
|
+
|
|
1521
|
+
# Check for unresolved dependencies
|
|
1522
|
+
if dependencies.unresolved:
|
|
1523
|
+
unresolved_args = [name for name in dependencies.unresolved.keys()]
|
|
1524
|
+
raise OrionisContainerException(
|
|
1525
|
+
f"Cannot resolve '{name}' because the following required arguments are missing: [{', '.join(unresolved_args)}]."
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
# Resolve dependencies
|
|
1529
|
+
params = {}
|
|
1530
|
+
for param_name, dep in dependencies.resolved.items():
|
|
1531
|
+
params[param_name] = self.__resolveSingleDependency(name, param_name, dep)
|
|
1532
|
+
|
|
1533
|
+
return params
|
|
1534
|
+
|
|
1535
|
+
except Exception as e:
|
|
1536
|
+
# If the exception is already an OrionisContainerException, re-raise it
|
|
1537
|
+
if isinstance(e, OrionisContainerException):
|
|
1538
|
+
raise e from None
|
|
1539
|
+
|
|
1540
|
+
# Otherwise, raise a new OrionisContainerException with additional context
|
|
1541
|
+
raise OrionisContainerException(
|
|
1542
|
+
f"Error resolving dependencies for '{name}': {str(e)}"
|
|
1543
|
+
) from e
|
|
1544
|
+
|
|
1545
|
+
def __resolveSingleDependency(
|
|
1546
|
+
self,
|
|
1547
|
+
name: str,
|
|
1548
|
+
param_name: str,
|
|
1549
|
+
dependency: Argument
|
|
1550
|
+
) -> Any:
|
|
1551
|
+
"""
|
|
1552
|
+
Resolves a single dependency parameter.
|
|
1553
|
+
|
|
1554
|
+
This method centralizes the logic for resolving individual dependencies,
|
|
1555
|
+
following a priority order: default values, container bindings, auto-resolution,
|
|
1556
|
+
direct instantiation, and callable invocation.
|
|
1557
|
+
|
|
1558
|
+
Parameters
|
|
1559
|
+
----------
|
|
1560
|
+
name : str
|
|
1561
|
+
The name of the class or callable being resolved (for error context).
|
|
1562
|
+
param_name : str
|
|
1563
|
+
The name of the parameter being resolved.
|
|
1564
|
+
dependency : Argument
|
|
1565
|
+
The dependency argument object containing type and metadata.
|
|
1566
|
+
|
|
1567
|
+
Returns
|
|
1568
|
+
-------
|
|
1569
|
+
Any
|
|
1570
|
+
The resolved dependency instance or value.
|
|
1571
|
+
|
|
1572
|
+
Raises
|
|
1573
|
+
------
|
|
1574
|
+
OrionisContainerException
|
|
1575
|
+
If the dependency cannot be resolved through any available method.
|
|
1576
|
+
"""
|
|
1577
|
+
|
|
1578
|
+
# If the dependency has a default value, use it
|
|
1579
|
+
if dependency.default is not None:
|
|
1580
|
+
return dependency.default
|
|
1581
|
+
|
|
1582
|
+
# If the dependency is a built-in type, raise an exception
|
|
1583
|
+
if dependency.module_name == 'builtins':
|
|
1584
|
+
raise OrionisContainerException(
|
|
1585
|
+
f"Cannot resolve '{name}' because parameter '{param_name}' depends on built-in type '{dependency.type.__name__}'."
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
# Try to resolve from container using type (Abstract or Interface)
|
|
1589
|
+
if self.bound(dependency.type):
|
|
1590
|
+
return self.resolve(self.getBinding(dependency.type))
|
|
1591
|
+
|
|
1592
|
+
# Try to resolve from container using full class path
|
|
1593
|
+
if self.bound(dependency.full_class_path):
|
|
1594
|
+
return self.resolve(self.getBinding(dependency.full_class_path))
|
|
1595
|
+
|
|
1596
|
+
# Try auto-resolution first
|
|
1597
|
+
if self.__canAutoResolve(dependency.type):
|
|
1598
|
+
return self.__autoResolve(dependency.type)
|
|
1599
|
+
|
|
1600
|
+
# Try to instantiate directly if it's a concrete class
|
|
1601
|
+
if ReflectionConcrete.isConcreteClass(dependency.type):
|
|
1602
|
+
return self.__instantiateWithReflection(dependency.type, is_class=True)
|
|
1603
|
+
|
|
1604
|
+
# Try to call directly if it's a callable
|
|
1605
|
+
if callable(dependency.type) and not isinstance(dependency.type, type):
|
|
1606
|
+
return self.__instantiateWithReflection(dependency.type, is_class=False)
|
|
1607
|
+
|
|
1608
|
+
# If the dependency cannot be resolved, raise an exception
|
|
1609
|
+
raise OrionisContainerException(
|
|
1610
|
+
f"Cannot resolve dependency '{param_name}' of type '{dependency.type.__name__}' for '{name}'."
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
def __instantiateWithReflection(
|
|
1614
|
+
self,
|
|
1615
|
+
target: Callable[..., Any],
|
|
1616
|
+
is_class: bool = True
|
|
1617
|
+
) -> Any:
|
|
1618
|
+
"""
|
|
1619
|
+
Helper method to instantiate a target with reflection-based dependency resolution.
|
|
1620
|
+
|
|
1621
|
+
Parameters
|
|
1622
|
+
----------
|
|
1623
|
+
target : Callable[..., Any]
|
|
1624
|
+
The class or callable to instantiate.
|
|
1625
|
+
is_class : bool
|
|
1626
|
+
Whether the target is a class (True) or callable (False).
|
|
1627
|
+
|
|
1628
|
+
Returns
|
|
1629
|
+
-------
|
|
1630
|
+
Any
|
|
1631
|
+
The instantiated object or callable result.
|
|
1632
|
+
"""
|
|
1633
|
+
|
|
1634
|
+
resolved_params = self.__resolveDependencies(
|
|
1635
|
+
*self.__reflectTarget(target, is_class=is_class)
|
|
1636
|
+
)
|
|
1637
|
+
return target(**resolved_params)
|
|
1638
|
+
|
|
1639
|
+
def resolveWithoutContainer(
|
|
1640
|
+
self,
|
|
1641
|
+
type_: Callable[..., Any],
|
|
1642
|
+
*args,
|
|
1643
|
+
**kwargs
|
|
1644
|
+
) -> Any:
|
|
1645
|
+
"""
|
|
1646
|
+
Forces resolution of a type whether it's registered in the container or not.
|
|
1647
|
+
|
|
1648
|
+
Parameters
|
|
1649
|
+
----------
|
|
1650
|
+
type_ : Callable[..., Any]
|
|
1651
|
+
The type or callable to resolve.
|
|
1652
|
+
*args : tuple
|
|
1653
|
+
Positional arguments to pass to the constructor/callable.
|
|
1654
|
+
**kwargs : dict
|
|
1655
|
+
Keyword arguments to pass to the constructor/callable.
|
|
1656
|
+
|
|
1657
|
+
Returns
|
|
1658
|
+
-------
|
|
1659
|
+
Any
|
|
1660
|
+
The resolved instance.
|
|
1661
|
+
|
|
1662
|
+
Raises
|
|
1663
|
+
------
|
|
1664
|
+
OrionisContainerException
|
|
1665
|
+
If the type cannot be resolved.
|
|
1666
|
+
"""
|
|
1667
|
+
try:
|
|
1668
|
+
# If args or kwargs are provided, use them directly
|
|
1669
|
+
if args or kwargs:
|
|
1670
|
+
if isinstance(type_, type):
|
|
1671
|
+
return type_(*args, **kwargs)
|
|
1672
|
+
elif callable(type_):
|
|
1673
|
+
return type_(*args, **kwargs)
|
|
1674
|
+
|
|
1675
|
+
# Try auto-resolution first for unregistered types
|
|
1676
|
+
if self.__canAutoResolve(type_):
|
|
1677
|
+
return self.__autoResolve(type_)
|
|
1678
|
+
|
|
1679
|
+
# Use the unified reflection-based instantiation
|
|
1680
|
+
if ReflectionConcrete.isConcreteClass(type_):
|
|
1681
|
+
return self.__instantiateWithReflection(type_, is_class=True)
|
|
1682
|
+
|
|
1683
|
+
if callable(type_) and not isinstance(type_, type):
|
|
1684
|
+
return self.__instantiateWithReflection(type_, is_class=False)
|
|
1685
|
+
|
|
1686
|
+
# If the type is neither a concrete class nor a callable, raise an exception
|
|
1687
|
+
raise OrionisContainerException(
|
|
1688
|
+
f"Cannot force resolve: {getattr(type_, '__name__', str(type_))} is neither a concrete class nor a callable."
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
except Exception as e:
|
|
1692
|
+
if isinstance(e, OrionisContainerException):
|
|
1693
|
+
raise e from None
|
|
1694
|
+
|
|
1695
|
+
raise OrionisContainerException(
|
|
1696
|
+
f"Error resolving '{getattr(type_, '__name__', str(type_))}': {str(e)}"
|
|
1697
|
+
) from e
|
|
1698
|
+
|
|
1699
|
+
def __canAutoResolve(
|
|
1700
|
+
self,
|
|
1701
|
+
type_: Callable[..., Any]
|
|
1702
|
+
) -> bool:
|
|
1703
|
+
"""
|
|
1704
|
+
Check if a type can be automatically resolved by the container.
|
|
1705
|
+
|
|
1706
|
+
This method determines whether a given type meets all the criteria for
|
|
1707
|
+
automatic dependency resolution. For a type to be auto-resolvable, it must
|
|
1708
|
+
be a concrete class that belongs to a valid application namespace, is
|
|
1709
|
+
not a built-in Python type, and is actually instantiable.
|
|
1710
|
+
|
|
1711
|
+
The validation process includes checking that the type is:
|
|
1712
|
+
- A proper class (not a function, module, or other callable)
|
|
1713
|
+
- Not a built-in Python type (str, int, list, etc.)
|
|
1714
|
+
- A concrete class (not abstract or interface)
|
|
1715
|
+
- Not a generic type or type variable
|
|
1716
|
+
- Part of a valid namespace defined in the container configuration
|
|
1717
|
+
- Actually instantiable (has a callable constructor)
|
|
1718
|
+
|
|
1719
|
+
Parameters
|
|
1720
|
+
----------
|
|
1721
|
+
type_ : Callable[..., Any]
|
|
1722
|
+
The type to check for auto-resolution eligibility. This should be
|
|
1723
|
+
a class or callable that represents the type to be validated for
|
|
1724
|
+
automatic dependency injection.
|
|
1725
|
+
|
|
1726
|
+
Returns
|
|
1727
|
+
-------
|
|
1728
|
+
bool
|
|
1729
|
+
True if the type can be automatically resolved by the container,
|
|
1730
|
+
False otherwise. Returns True only when all validation criteria
|
|
1731
|
+
are met: the type is a concrete, instantiable class from a valid
|
|
1732
|
+
namespace and is not a built-in type.
|
|
1733
|
+
"""
|
|
1734
|
+
|
|
1735
|
+
# Check if the provided parameter is actually a class type
|
|
1736
|
+
# Functions, modules, and other callables are not eligible for auto-resolution
|
|
1737
|
+
if not isinstance(type_, type):
|
|
1738
|
+
return False
|
|
1739
|
+
|
|
1740
|
+
# Exclude built-in Python types (str, int, list, dict, etc.)
|
|
1741
|
+
# These types cannot be auto-resolved as they require explicit values
|
|
1742
|
+
if hasattr(type_, '__module__') and type_.__module__ == 'builtins':
|
|
1743
|
+
return False
|
|
1744
|
+
|
|
1745
|
+
# Check if the type belongs to a valid namespace for auto-resolution
|
|
1746
|
+
# Only classes from configured application namespaces are allowed
|
|
1747
|
+
if not self.__isValidNamespace(type_):
|
|
1748
|
+
return False
|
|
1749
|
+
|
|
1750
|
+
# Check if the type is actually instantiable
|
|
1751
|
+
if not self.__isInstantiable(type_):
|
|
1752
|
+
return False
|
|
1753
|
+
|
|
1754
|
+
# All checks passed - type can be auto-resolved
|
|
1755
|
+
return True
|
|
1756
|
+
|
|
1757
|
+
def __isValidNamespace(self, type_: type) -> bool:
|
|
1758
|
+
"""
|
|
1759
|
+
Checks if a type belongs to a valid namespace for auto-resolution.
|
|
1760
|
+
|
|
1761
|
+
Parameters
|
|
1762
|
+
----------
|
|
1763
|
+
type_ : type
|
|
1764
|
+
The type to check for valid namespace.
|
|
1765
|
+
|
|
1766
|
+
Returns
|
|
1767
|
+
-------
|
|
1768
|
+
bool
|
|
1769
|
+
True if the type belongs to a valid namespace, False otherwise.
|
|
1770
|
+
"""
|
|
1771
|
+
|
|
1772
|
+
# Verify that the module name starts with any of the valid namespace prefixes
|
|
1773
|
+
if hasattr(type_, '__module__'):
|
|
1774
|
+
module_name = type_.__module__
|
|
1775
|
+
return any(module_name.startswith(namespace) for namespace in self.__valid_namespaces)
|
|
1776
|
+
|
|
1777
|
+
# If the type has no module information, it cannot be auto-resolved
|
|
1778
|
+
return False
|
|
1779
|
+
|
|
1780
|
+
def __isInstantiable(self, type_: type) -> bool:
|
|
1781
|
+
"""
|
|
1782
|
+
Checks if a type is actually instantiable (not abstract, not generic, etc.).
|
|
1783
|
+
|
|
1784
|
+
This method performs comprehensive checks to determine if a class can be
|
|
1785
|
+
safely instantiated through auto-resolution. It validates that the class
|
|
1786
|
+
is concrete, not abstract, not a generic type, and has a callable constructor.
|
|
1787
|
+
|
|
1788
|
+
Parameters
|
|
1789
|
+
----------
|
|
1790
|
+
type_ : type
|
|
1791
|
+
The type to check for instantiability.
|
|
1792
|
+
|
|
1793
|
+
Returns
|
|
1794
|
+
-------
|
|
1795
|
+
bool
|
|
1796
|
+
True if the type can be instantiated, False otherwise.
|
|
1797
|
+
"""
|
|
1798
|
+
|
|
1799
|
+
try:
|
|
1800
|
+
|
|
1801
|
+
# 1. Check if it's a concrete class using ReflectionConcrete
|
|
1802
|
+
if not ReflectionConcrete.isConcreteClass(type_):
|
|
1803
|
+
return False
|
|
1804
|
+
|
|
1805
|
+
# 2. Check if it's an abstract class or has abstract methods
|
|
1806
|
+
if self.__isAbstractClass(type_):
|
|
1807
|
+
return False
|
|
1808
|
+
|
|
1809
|
+
# 3. Check if it's a generic type (like List[T], Dict[K,V])
|
|
1810
|
+
if self.__isGenericType(type_):
|
|
1811
|
+
return False
|
|
1812
|
+
|
|
1813
|
+
# 4. Check if it's a protocol or typing construct
|
|
1814
|
+
if self.__isProtocolOrTyping(type_):
|
|
1815
|
+
return False
|
|
1816
|
+
|
|
1817
|
+
# 5. Check if the constructor is callable
|
|
1818
|
+
if not hasattr(type_, '__init__'):
|
|
1819
|
+
return False
|
|
1820
|
+
|
|
1821
|
+
# 6. Basic instantiation test with empty args (if no required params)
|
|
1822
|
+
if self.__hasRequiredConstructorParams(type_):
|
|
1823
|
+
# If it has required params, we can still auto-resolve if they can be resolved
|
|
1824
|
+
return True
|
|
1825
|
+
else:
|
|
1826
|
+
# If no required params, try a quick instantiation test
|
|
1827
|
+
return self.__canQuickInstantiate(type_)
|
|
1828
|
+
|
|
1829
|
+
except Exception:
|
|
1830
|
+
# If any check fails with an exception, consider it non-instantiable
|
|
1831
|
+
return False
|
|
1832
|
+
|
|
1833
|
+
def __isAbstractClass(self, type_: type) -> bool:
|
|
1834
|
+
"""
|
|
1835
|
+
Checks if a type is an abstract class.
|
|
1836
|
+
|
|
1837
|
+
Parameters
|
|
1838
|
+
----------
|
|
1839
|
+
type_ : type
|
|
1840
|
+
The type to check.
|
|
1841
|
+
|
|
1842
|
+
Returns
|
|
1843
|
+
-------
|
|
1844
|
+
bool
|
|
1845
|
+
True if the type is abstract, False otherwise.
|
|
1846
|
+
"""
|
|
1847
|
+
|
|
1848
|
+
# Check if it's explicitly marked as abstract
|
|
1849
|
+
if hasattr(type_, '__abstractmethods__') and type_.__abstractmethods__:
|
|
1850
|
+
return True
|
|
1851
|
+
|
|
1852
|
+
# Check if it inherits from ABC (safely)
|
|
1853
|
+
try:
|
|
1854
|
+
if issubclass(type_, abc.ABC):
|
|
1855
|
+
return True
|
|
1856
|
+
except TypeError:
|
|
1857
|
+
# type_ is not a class, so it can't be abstract
|
|
1858
|
+
pass
|
|
1859
|
+
|
|
1860
|
+
# Check if it has abstract methods
|
|
1861
|
+
try:
|
|
1862
|
+
# Try to get abstract methods using reflection
|
|
1863
|
+
for attr_name in dir(type_):
|
|
1864
|
+
attr = getattr(type_, attr_name)
|
|
1865
|
+
if hasattr(attr, '__isabstractmethod__') and attr.__isabstractmethod__:
|
|
1866
|
+
return True
|
|
1867
|
+
except Exception:
|
|
1868
|
+
pass
|
|
1869
|
+
|
|
1870
|
+
return False
|
|
1871
|
+
|
|
1872
|
+
def __isGenericType(self, type_: type) -> bool:
|
|
1873
|
+
"""
|
|
1874
|
+
Checks if a type is a generic type (e.g., List[T], Dict[K,V]).
|
|
1875
|
+
|
|
1876
|
+
Parameters
|
|
1877
|
+
----------
|
|
1878
|
+
type_ : type
|
|
1879
|
+
The type to check.
|
|
1880
|
+
|
|
1881
|
+
Returns
|
|
1882
|
+
-------
|
|
1883
|
+
bool
|
|
1884
|
+
True if the type is generic, False otherwise.
|
|
1885
|
+
"""
|
|
1886
|
+
|
|
1887
|
+
# Check for generic alias (Python 3.7+)
|
|
1888
|
+
if hasattr(typing, 'get_origin') and typing.get_origin(type_) is not None:
|
|
1889
|
+
return True
|
|
1890
|
+
|
|
1891
|
+
# Check for older style generic types
|
|
1892
|
+
if hasattr(type_, '__origin__'):
|
|
1893
|
+
return True
|
|
1894
|
+
|
|
1895
|
+
# Check if it's a typing construct
|
|
1896
|
+
if hasattr(typing, '_GenericAlias') and isinstance(type_, typing._GenericAlias):
|
|
1897
|
+
return True
|
|
1898
|
+
|
|
1899
|
+
# Check for type variables
|
|
1900
|
+
if hasattr(typing, 'TypeVar') and isinstance(type_, typing.TypeVar):
|
|
1901
|
+
return True
|
|
1902
|
+
|
|
1903
|
+
return False
|
|
1904
|
+
|
|
1905
|
+
def __isProtocolOrTyping(self, type_: type) -> bool:
|
|
1906
|
+
"""
|
|
1907
|
+
Checks if a type is a Protocol or other typing construct that shouldn't be instantiated.
|
|
1908
|
+
|
|
1909
|
+
Parameters
|
|
1910
|
+
----------
|
|
1911
|
+
type_ : type
|
|
1912
|
+
The type to check.
|
|
1913
|
+
|
|
1914
|
+
Returns
|
|
1915
|
+
-------
|
|
1916
|
+
bool
|
|
1917
|
+
True if the type is a protocol or typing construct, False otherwise.
|
|
1918
|
+
"""
|
|
1919
|
+
|
|
1920
|
+
# Check if it's a Protocol (Python 3.8+)
|
|
1921
|
+
try:
|
|
1922
|
+
if hasattr(typing, 'Protocol') and issubclass(type_, typing.Protocol):
|
|
1923
|
+
return True
|
|
1924
|
+
|
|
1925
|
+
# type_ is not a class, so it can't be a Protocol
|
|
1926
|
+
except TypeError:
|
|
1927
|
+
pass
|
|
1928
|
+
|
|
1929
|
+
# Check if it's in the typing module
|
|
1930
|
+
if hasattr(type_, '__module__') and type_.__module__ == 'typing':
|
|
1931
|
+
return True
|
|
1932
|
+
|
|
1933
|
+
# Check for common typing constructs that shouldn't be instantiated
|
|
1934
|
+
typing_constructs = ['Union', 'Optional', 'Any', 'Callable', 'Type']
|
|
1935
|
+
if hasattr(type_, '__name__') and type_.__name__ in typing_constructs:
|
|
1936
|
+
return True
|
|
1937
|
+
|
|
1938
|
+
return False
|
|
1939
|
+
|
|
1940
|
+
def __hasRequiredConstructorParams(self, type_: type) -> bool:
|
|
1941
|
+
"""
|
|
1942
|
+
Checks if a type has required constructor parameters.
|
|
1943
|
+
|
|
1944
|
+
Parameters
|
|
1945
|
+
----------
|
|
1946
|
+
type_ : type
|
|
1947
|
+
The type to check.
|
|
1948
|
+
|
|
1949
|
+
Returns
|
|
1950
|
+
-------
|
|
1951
|
+
bool
|
|
1952
|
+
True if the type has required constructor parameters, False otherwise.
|
|
1953
|
+
"""
|
|
1954
|
+
|
|
1955
|
+
try:
|
|
1956
|
+
|
|
1957
|
+
# Use reflection to get constructor dependencies
|
|
1958
|
+
reflection = ReflectionConcrete(type_)
|
|
1959
|
+
dependencies = reflection.getConstructorDependencies()
|
|
1960
|
+
|
|
1961
|
+
# Check if there are any unresolved dependencies or required parameters
|
|
1962
|
+
if dependencies and dependencies.unresolved:
|
|
1963
|
+
return True
|
|
1964
|
+
|
|
1965
|
+
# Check if there are resolved dependencies that don't have defaults and can't be resolved
|
|
1966
|
+
if dependencies and dependencies.resolved:
|
|
1967
|
+
for param_name, dep in dependencies.resolved.items():
|
|
1968
|
+
# Only consider it required if it has no default AND can't be resolved by container
|
|
1969
|
+
if dep.default is None and not self.bound(dep.type) and not self.bound(dep.full_class_path):
|
|
1970
|
+
return True
|
|
1971
|
+
|
|
1972
|
+
# If no unresolved dependencies and all resolved have defaults, return False
|
|
1973
|
+
return False
|
|
1974
|
+
|
|
1975
|
+
except Exception:
|
|
1976
|
+
|
|
1977
|
+
# If reflection fails, assume it has required params to be safe
|
|
1978
|
+
return True
|
|
1979
|
+
|
|
1980
|
+
def __canQuickInstantiate(self, type_: type) -> bool:
|
|
1981
|
+
"""
|
|
1982
|
+
Performs a quick instantiation test to verify the type can be created.
|
|
1983
|
+
|
|
1984
|
+
This method attempts to create an instance of the type without arguments
|
|
1985
|
+
to verify that it can be instantiated successfully.
|
|
1986
|
+
|
|
1987
|
+
Parameters
|
|
1988
|
+
----------
|
|
1989
|
+
type_ : type
|
|
1990
|
+
The type to test.
|
|
1991
|
+
|
|
1992
|
+
Returns
|
|
1993
|
+
-------
|
|
1994
|
+
bool
|
|
1995
|
+
True if the type can be instantiated, False otherwise.
|
|
1996
|
+
"""
|
|
1997
|
+
|
|
1998
|
+
try:
|
|
1999
|
+
# For safety, first check if the constructor signature suggests it's safe to instantiate
|
|
2000
|
+
try:
|
|
2001
|
+
|
|
2002
|
+
# Use inspect to get the constructor signature
|
|
2003
|
+
sig = inspect.signature(type_.__init__)
|
|
2004
|
+
|
|
2005
|
+
# If __init__ has required parameters beyond 'self', skip quick instantiation
|
|
2006
|
+
required_params = [
|
|
2007
|
+
p for name, p in sig.parameters.items()
|
|
2008
|
+
if name != 'self' and p.default == inspect.Parameter.empty
|
|
2009
|
+
]
|
|
2010
|
+
|
|
2011
|
+
# If there are required parameters, we cannot quick instantiate
|
|
2012
|
+
if required_params:
|
|
2013
|
+
return False
|
|
2014
|
+
|
|
2015
|
+
except (ValueError, TypeError):
|
|
2016
|
+
|
|
2017
|
+
# If we can't inspect the signature, assume it's not safe
|
|
2018
|
+
return False
|
|
2019
|
+
|
|
2020
|
+
# Attempt to create an instance only if it seems safe
|
|
2021
|
+
instance = type_()
|
|
2022
|
+
|
|
2023
|
+
# If successful, clean up and return True
|
|
2024
|
+
del instance
|
|
2025
|
+
return True
|
|
2026
|
+
|
|
2027
|
+
except Exception:
|
|
2028
|
+
|
|
2029
|
+
# If instantiation fails for any reason, it's not auto-resolvable
|
|
2030
|
+
return False
|
|
2031
|
+
|
|
2032
|
+
def __autoResolve(
|
|
2033
|
+
self,
|
|
2034
|
+
type_: Callable[..., Any],
|
|
2035
|
+
*args,
|
|
2036
|
+
**kwargs
|
|
2037
|
+
) -> Any:
|
|
2038
|
+
"""
|
|
2039
|
+
Automatically resolves and instantiates a type with its dependencies.
|
|
2040
|
+
|
|
2041
|
+
Parameters
|
|
2042
|
+
----------
|
|
2043
|
+
type_ : Callable[..., Any]
|
|
2044
|
+
The class or callable to auto-resolve.
|
|
2045
|
+
*args : tuple
|
|
2046
|
+
Positional arguments to pass directly to the constructor or callable.
|
|
2047
|
+
**kwargs : dict
|
|
2048
|
+
Keyword arguments to pass directly to the constructor or callable.
|
|
2049
|
+
|
|
2050
|
+
Returns
|
|
2051
|
+
-------
|
|
2052
|
+
Any
|
|
2053
|
+
An instance of the requested type, with all dependencies resolved recursively.
|
|
2054
|
+
|
|
2055
|
+
Raises
|
|
2056
|
+
------
|
|
2057
|
+
OrionisContainerException
|
|
2058
|
+
If the type cannot be auto-resolved or if circular dependencies are detected.
|
|
2059
|
+
"""
|
|
2060
|
+
|
|
2061
|
+
# Build a unique key for the type to track resolution and detect circular dependencies
|
|
2062
|
+
type_key = f"{type_.__module__}.{type_.__name__}"
|
|
2063
|
+
if type_key in self.__resolution_cache:
|
|
2064
|
+
raise OrionisContainerException(
|
|
2065
|
+
f"Circular dependency detected while auto-resolving '{type_.__name__}'"
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
try:
|
|
2069
|
+
# Mark this type as being resolved to prevent circular dependencies
|
|
2070
|
+
self.__resolution_cache[type_key] = True
|
|
2071
|
+
|
|
2072
|
+
# Validate that the type can still be auto-resolved at resolution time
|
|
2073
|
+
if not self.__canAutoResolve(type_):
|
|
2074
|
+
raise OrionisContainerException(
|
|
2075
|
+
f"Type '{type_.__name__}' cannot be auto-resolved. "
|
|
2076
|
+
f"It may be abstract, generic, or not from a valid namespace."
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
# If explicit arguments are provided, instantiate/call directly
|
|
2080
|
+
if args or kwargs:
|
|
2081
|
+
return type_(*args, **kwargs)
|
|
2082
|
+
|
|
2083
|
+
# Use unified reflection-based instantiation
|
|
2084
|
+
if ReflectionConcrete.isConcreteClass(type_):
|
|
2085
|
+
return self.__instantiateWithReflection(type_, is_class=True)
|
|
2086
|
+
elif callable(type_) and not isinstance(type_, type):
|
|
2087
|
+
return self.__instantiateWithReflection(type_, is_class=False)
|
|
2088
|
+
else:
|
|
2089
|
+
raise OrionisContainerException(
|
|
2090
|
+
f"Type '{type_.__name__}' is not a concrete class or callable"
|
|
2091
|
+
)
|
|
2092
|
+
|
|
2093
|
+
except Exception as e:
|
|
2094
|
+
# Remove the type from the resolution cache on error
|
|
2095
|
+
self.__resolution_cache.pop(type_key, None)
|
|
2096
|
+
|
|
2097
|
+
if isinstance(e, OrionisContainerException):
|
|
2098
|
+
raise
|
|
2099
|
+
else:
|
|
2100
|
+
raise OrionisContainerException(
|
|
2101
|
+
f"Failed to auto-resolve '{type_.__name__}': {str(e)}"
|
|
2102
|
+
) from e
|
|
2103
|
+
|
|
2104
|
+
finally:
|
|
2105
|
+
# Always clean up the resolution cache after resolution attempt
|
|
2106
|
+
self.__resolution_cache.pop(type_key, None)
|
|
2107
|
+
|
|
2108
|
+
def call(
|
|
2109
|
+
self,
|
|
2110
|
+
instance: Any,
|
|
2111
|
+
method_name: str,
|
|
2112
|
+
*args,
|
|
2113
|
+
**kwargs
|
|
2114
|
+
) -> Any:
|
|
2115
|
+
"""
|
|
2116
|
+
Call a method on an instance with automatic dependency injection.
|
|
2117
|
+
|
|
2118
|
+
Parameters
|
|
2119
|
+
----------
|
|
2120
|
+
instance : Any
|
|
2121
|
+
The instance on which to call the method.
|
|
2122
|
+
method_name : str
|
|
2123
|
+
The name of the method to call.
|
|
2124
|
+
*args : tuple
|
|
2125
|
+
Positional arguments to pass to the method.
|
|
2126
|
+
**kwargs : dict
|
|
2127
|
+
Keyword arguments to pass to the method.
|
|
2128
|
+
|
|
2129
|
+
Returns
|
|
2130
|
+
-------
|
|
2131
|
+
Any
|
|
2132
|
+
The result of the method call.
|
|
2133
|
+
"""
|
|
2134
|
+
|
|
2135
|
+
# Ensure the instance is a valid object (allow __main__ for development)
|
|
2136
|
+
if instance is None:
|
|
2137
|
+
raise OrionisContainerException("Instance cannot be None")
|
|
2138
|
+
|
|
2139
|
+
if not hasattr(instance, '__class__'):
|
|
2140
|
+
raise OrionisContainerException("Instance must be a valid object with a class")
|
|
2141
|
+
|
|
2142
|
+
# Validate method_name parameter
|
|
2143
|
+
if not isinstance(method_name, str):
|
|
2144
|
+
raise OrionisContainerException(
|
|
2145
|
+
f"Method name must be a string, got {type(method_name).__name__}"
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
if not method_name.strip():
|
|
2149
|
+
raise OrionisContainerException(
|
|
2150
|
+
"Method name cannot be empty or whitespace"
|
|
2151
|
+
)
|
|
2152
|
+
|
|
2153
|
+
# Ensure the method exists and is callable
|
|
2154
|
+
method = getattr(instance, method_name, None)
|
|
2155
|
+
if not callable(method):
|
|
2156
|
+
raise OrionisContainerException(
|
|
2157
|
+
f"Method '{method_name}' not found or not callable on instance '{type(instance).__name__}'."
|
|
2158
|
+
)
|
|
2159
|
+
|
|
2160
|
+
# If args or kwargs are provided, use them directly
|
|
2161
|
+
if args or kwargs:
|
|
2162
|
+
return self.__instantiateCallableWithArgs(method, *args, **kwargs)
|
|
2163
|
+
|
|
2164
|
+
# For methods without provided arguments, try simple call first
|
|
2165
|
+
# This avoids reflection issues for simple methods
|
|
2166
|
+
try:
|
|
2167
|
+
|
|
2168
|
+
# Get method signature to check if it needs parameters
|
|
2169
|
+
sig = inspect.signature(method)
|
|
2170
|
+
|
|
2171
|
+
# Filter out 'self' parameter for bound methods
|
|
2172
|
+
params = [p for name, p in sig.parameters.items()
|
|
2173
|
+
if name != 'self' and p.default == inspect.Parameter.empty]
|
|
2174
|
+
|
|
2175
|
+
# If no required parameters, call directly
|
|
2176
|
+
if not params:
|
|
2177
|
+
return method()
|
|
2178
|
+
|
|
2179
|
+
# If has required parameters, try dependency injection
|
|
2180
|
+
return self.__instantiateWithReflection(method, is_class=False)
|
|
2181
|
+
|
|
2182
|
+
except Exception as reflection_error:
|
|
2183
|
+
|
|
2184
|
+
# If reflection fails, try simple call as fallback
|
|
2185
|
+
try:
|
|
2186
|
+
return method()
|
|
2187
|
+
|
|
2188
|
+
# If simple call also fails, raise the original reflection error
|
|
2189
|
+
except TypeError:
|
|
2190
|
+
raise OrionisContainerException(
|
|
2191
|
+
f"Failed to call method '{method_name}': {reflection_error}"
|
|
2192
|
+
) from reflection_error
|