orionis 0.450.0__py3-none-any.whl → 0.452.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.
Files changed (43) hide show
  1. orionis/console/base/contracts/command.py +0 -2
  2. orionis/console/core/__init__.py +0 -0
  3. orionis/console/core/reactor.py +19 -4
  4. orionis/container/container.py +1581 -69
  5. orionis/container/contracts/container.py +184 -33
  6. orionis/foundation/config/database/entities/mysql.py +0 -1
  7. orionis/metadata/framework.py +1 -1
  8. orionis/services/inspirational/contracts/__init__.py +0 -0
  9. orionis/services/introspection/abstract/contracts/reflection.py +5 -6
  10. orionis/services/introspection/abstract/reflection.py +5 -6
  11. orionis/services/introspection/callables/contracts/reflection.py +3 -2
  12. orionis/services/introspection/callables/reflection.py +4 -4
  13. orionis/services/introspection/concretes/contracts/reflection.py +5 -6
  14. orionis/services/introspection/concretes/reflection.py +5 -6
  15. orionis/services/introspection/dependencies/contracts/reflection.py +87 -23
  16. orionis/services/introspection/dependencies/entities/argument.py +95 -0
  17. orionis/services/introspection/dependencies/entities/resolve_argument.py +82 -0
  18. orionis/services/introspection/dependencies/reflection.py +176 -106
  19. orionis/services/introspection/instances/contracts/reflection.py +5 -6
  20. orionis/services/introspection/instances/reflection.py +5 -6
  21. orionis/test/core/unit_test.py +150 -48
  22. {orionis-0.450.0.dist-info → orionis-0.452.0.dist-info}/METADATA +1 -1
  23. {orionis-0.450.0.dist-info → orionis-0.452.0.dist-info}/RECORD +35 -38
  24. tests/container/mocks/mock_auto_resolution.py +192 -0
  25. tests/services/introspection/dependencies/test_reflect_dependencies.py +135 -58
  26. tests/services/introspection/reflection/test_reflection_abstract.py +5 -4
  27. tests/services/introspection/reflection/test_reflection_callable.py +3 -3
  28. tests/services/introspection/reflection/test_reflection_concrete.py +4 -4
  29. tests/services/introspection/reflection/test_reflection_instance.py +5 -5
  30. orionis/console/args/parser.py +0 -40
  31. orionis/container/contracts/resolver.py +0 -115
  32. orionis/container/resolver/resolver.py +0 -602
  33. orionis/services/introspection/dependencies/entities/callable_dependencies.py +0 -54
  34. orionis/services/introspection/dependencies/entities/class_dependencies.py +0 -61
  35. orionis/services/introspection/dependencies/entities/known_dependencies.py +0 -67
  36. orionis/services/introspection/dependencies/entities/method_dependencies.py +0 -61
  37. tests/container/resolver/test_resolver.py +0 -62
  38. /orionis/{container/resolver → console/commands}/__init__.py +0 -0
  39. {tests/container/resolver → orionis/console/contracts}/__init__.py +0 -0
  40. {orionis-0.450.0.dist-info → orionis-0.452.0.dist-info}/WHEEL +0 -0
  41. {orionis-0.450.0.dist-info → orionis-0.452.0.dist-info}/licenses/LICENCE +0 -0
  42. {orionis-0.450.0.dist-info → orionis-0.452.0.dist-info}/top_level.txt +0 -0
  43. {orionis-0.450.0.dist-info → orionis-0.452.0.dist-info}/zip-safe +0 -0
@@ -1,14 +1,21 @@
1
+ import abc
2
+ import inspect
1
3
  import threading
4
+ import typing
5
+ from pathlib import Path
2
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.resolver.resolver import Resolver
9
13
  from orionis.container.validators import *
10
14
  from orionis.services.introspection.abstract.reflection import ReflectionAbstract
11
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
12
19
 
13
20
  class Container(IContainer):
14
21
 
@@ -75,7 +82,7 @@ class Container(IContainer):
75
82
  Notes
76
83
  -----
77
84
  - The `__bindings` dictionary is used to store service bindings.
78
- - The `__aliasses` dictionary is used to store service aliases.
85
+ - The `__aliases` dictionary is used to store service aliases.
79
86
  - Initialization occurs only once per instance, regardless of how many times __init__ is called.
80
87
  - The container registers itself under the IContainer interface to allow for dependency injection.
81
88
  """
@@ -83,9 +90,18 @@ class Container(IContainer):
83
90
  # Check if the instance has already been initialized
84
91
  if not hasattr(self, '_Container__initialized'):
85
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
+
86
100
  # Initialize the container's internal state
87
101
  self.__bindings = {}
88
- self.__aliasses = {}
102
+ self.__aliases = {}
103
+ self.__resolution_cache = {}
104
+ self.__singleton_cache = {}
89
105
 
90
106
  # Mark this instance as initialized
91
107
  self.__initialized = True
@@ -171,7 +187,7 @@ class Container(IContainer):
171
187
  )
172
188
 
173
189
  # Register the alias
174
- self.__aliasses[alias] = self.__bindings[abstract]
190
+ self.__aliases[alias] = self.__bindings[abstract]
175
191
 
176
192
  # Return True to indicate successful registration
177
193
  return True
@@ -254,7 +270,7 @@ class Container(IContainer):
254
270
  )
255
271
 
256
272
  # Register the alias
257
- self.__aliasses[alias] = self.__bindings[abstract]
273
+ self.__aliases[alias] = self.__bindings[abstract]
258
274
 
259
275
  # Return True to indicate successful registration
260
276
  return True
@@ -337,7 +353,7 @@ class Container(IContainer):
337
353
  )
338
354
 
339
355
  # Register the alias
340
- self.__aliasses[alias] = self.__bindings[abstract]
356
+ self.__aliases[alias] = self.__bindings[abstract]
341
357
 
342
358
  # Return True to indicate successful registration
343
359
  return True
@@ -418,7 +434,7 @@ class Container(IContainer):
418
434
  )
419
435
 
420
436
  # Register the alias
421
- self.__aliasses[alias] = self.__bindings[abstract]
437
+ self.__aliases[alias] = self.__bindings[abstract]
422
438
 
423
439
  # Return True to indicate successful registration
424
440
  return True
@@ -465,7 +481,7 @@ class Container(IContainer):
465
481
  IsCallable(fn)
466
482
 
467
483
  # Inspect the function signature
468
- params = ReflectionCallable(fn).getDependencies()
484
+ params: ResolveArguments = ReflectionCallable(fn).getDependencies()
469
485
 
470
486
  # If the function requires arguments, only allow TRANSIENT
471
487
  if (len(params.resolved) + len(params.unresolved)) > 0 and lifetime != Lifetime.TRANSIENT:
@@ -484,7 +500,7 @@ class Container(IContainer):
484
500
  )
485
501
 
486
502
  # Register the function as a binding
487
- self.__aliasses[alias] = self.__bindings[alias]
503
+ self.__aliases[alias] = self.__bindings[alias]
488
504
 
489
505
  return True
490
506
 
@@ -503,17 +519,22 @@ class Container(IContainer):
503
519
  Returns
504
520
  -------
505
521
  bool
506
- True if the service is registered (either as an abstract type or alias), False otherwise.
522
+ True if the service is registered and has a valid binding, False otherwise.
507
523
 
508
524
  Notes
509
525
  -----
510
- This method allows you to verify whether a service has been registered in the container,
511
- either by its abstract type or by its alias. It supports both class-based and string-based lookups.
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.
512
529
  """
513
- return (
514
- abstract_or_alias in self.__bindings
515
- or abstract_or_alias in self.__aliasses
516
- )
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
517
538
 
518
539
  def getBinding(
519
540
  self,
@@ -542,143 +563,1634 @@ class Container(IContainer):
542
563
  The binding contains information about the service registration including
543
564
  the concrete implementation, lifetime, and other configuration details.
544
565
 
566
+ Raises
567
+ ------
568
+ OrionisContainerException
569
+ If a binding is found but is corrupted or incomplete.
570
+
545
571
  Notes
546
572
  -----
547
573
  The method searches in the following order:
548
574
  1. Direct lookup in the bindings dictionary using the abstract type
549
575
  2. Lookup in the aliases dictionary using the provided alias
550
576
 
551
- This method does not raise exceptions for missing bindings; it returns None
552
- instead, allowing the caller to handle the absence of a binding appropriately.
577
+ This method validates binding integrity before returning it to ensure
578
+ that only valid, complete bindings are returned to the caller.
553
579
  """
554
580
 
555
581
  # First, attempt to find the binding directly using the abstract type
556
582
  # This handles cases where the service was registered with its abstract class
557
583
  binding = self.__bindings.get(abstract_or_alias)
558
584
  if binding:
585
+ self.__validateBinding(binding, abstract_or_alias)
559
586
  return binding
560
587
 
561
588
  # If no direct binding found, search in the aliases dictionary
562
589
  # This handles cases where the service is being requested by its alias
563
- binding = self.__aliasses.get(abstract_or_alias)
590
+ binding = self.__aliases.get(abstract_or_alias)
564
591
  if binding:
592
+ self.__validateBinding(binding, abstract_or_alias)
565
593
  return binding
566
594
 
567
595
  # Return None if no binding is found for the requested abstract type or alias
568
596
  return None
569
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.
693
+ """
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
+ )
740
+
570
741
  def drop(
571
742
  self,
572
743
  abstract: Callable[..., Any] = None,
573
744
  alias: str = None
574
- ) -> None:
745
+ ) -> bool:
575
746
  """
576
- Drops a service from the container by removing its bindings and aliases.
577
- This method allows removing registered services from the dependency injection container,
578
- either by their abstract type or by their alias. When a service is dropped,
579
- all its bindings and aliases are removed from the container.
747
+ Removes a service registration from the container by abstract type or alias.
580
748
 
581
- Warning
582
- -------
583
- Using this method irresponsibly can severely damage the system's logic.
584
- Only use it when you are certain about the consequences, as removing
585
- critical services may lead to system failures and unexpected behavior.
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
+ ----------
586
756
  abstract : Callable[..., Any], optional
587
- The abstract type or interface to be removed from the container.
588
- If provided, both the binding and the default alias for this type will be removed.
589
- The alias of the service to be removed. If provided, both the alias entry
590
- and any associated binding will be removed.
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.
591
776
 
592
777
  Notes
593
778
  -----
594
- At least one parameter (abstract or alias) must be provided for the method to take effect.
595
- If both are provided, both will be processed independently.
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
596
784
  """
597
785
 
786
+ deleted = False
787
+
598
788
  # If abstract is provided
599
789
  if abstract:
600
790
 
601
791
  # Remove the abstract service from the bindings if it exists
602
792
  if abstract in self.__bindings:
603
793
  del self.__bindings[abstract]
794
+ deleted = True
604
795
 
605
796
  # Remove the default alias (module + class name) from aliases if it exists
606
797
  abs_alias = ReflectionAbstract(abstract).getModuleWithClassName()
607
- if abs_alias in self.__aliasses:
608
- del self.__aliasses[abs_alias]
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
609
813
 
610
814
  # If a custom alias is provided
611
815
  if alias:
612
816
 
613
817
  # Remove it from the aliases dictionary if it exists
614
- if alias in self.__aliasses:
615
- del self.__aliasses[alias]
818
+ if alias in self.__aliases:
819
+ del self.__aliases[alias]
820
+ deleted = True
616
821
 
617
822
  # Remove the binding associated with the alias
618
823
  if alias in self.__bindings:
619
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()
620
859
 
621
860
  def make(
622
861
  self,
623
- abstract_or_alias: Any,
862
+ type_: Any,
624
863
  *args: tuple,
625
864
  **kwargs: dict
626
865
  ) -> Any:
627
866
  """
628
- Resolves and returns an instance of the requested service.
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.
629
874
 
630
875
  Parameters
631
876
  ----------
632
- abstract_or_alias : Any
633
- The abstract class, interface, or alias (str) to resolve.
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.
634
943
  *args : tuple
635
- Positional arguments to pass to the constructor of the resolved service.
944
+ Additional positional arguments to pass to the constructor.
636
945
  **kwargs : dict
637
- Keyword arguments to pass to the constructor of the resolved service.
946
+ Additional keyword arguments to pass to the constructor.
638
947
 
639
948
  Returns
640
949
  -------
641
950
  Any
642
- An instance of the requested service.
951
+ The resolved instance.
643
952
 
644
953
  Raises
645
954
  ------
646
955
  OrionisContainerException
647
- If the requested service is not registered in the container.
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.
648
1039
  """
649
- if not self.bound(abstract_or_alias):
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:
650
1083
  raise OrionisContainerException(
651
- f"The requested service '{abstract_or_alias}' is not registered in the container."
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."
652
1086
  )
653
1087
 
654
- # Get the binding for the requested abstract type or alias
655
- binding = self.getBinding(abstract_or_alias)
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.
656
1104
 
657
- # Resolve the binding using the Resolver class
658
- return Resolver(self).resolve(
659
- binding,
660
- *args,
661
- **kwargs
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
1119
+
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"
662
1127
  )
663
1128
 
664
- def createContext(self) -> ScopeManager:
1129
+ def __storeSingletonCrossReferences(self, binding: Binding, instance: Any) -> None:
665
1130
  """
666
- Creates a new context for managing scoped services.
1131
+ Stores cross-references for singleton instances to ensure discoverability.
667
1132
 
668
- This method returns a context manager that can be used with a 'with' statement
669
- to control the lifecycle of scoped services.
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.
670
1172
 
671
1173
  Returns
672
1174
  -------
673
- ScopeManager
674
- A context manager for scoped services.
1175
+ Any
1176
+ The scoped instance of the requested service.
675
1177
 
676
- Usage
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
677
1255
  -------
678
- with container.createContext():
679
- # Scoped services created here will be disposed when exiting this block
680
- service = container.make(IScopedService)
681
- ...
682
- # Scoped services are automatically disposed here
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:
1285
+ """
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.
683
1301
  """
684
- return ScopeManager()
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 with a default value, return it
1583
+ if dependency.module_name == 'builtins' and dependency.resolved:
1584
+ return dependency.default
1585
+
1586
+ # If the dependency is a built-in type, raise an exception
1587
+ elif dependency.module_name == 'builtins' and not dependency.resolved:
1588
+ raise OrionisContainerException(
1589
+ f"Cannot resolve '{name}' because parameter '{param_name}' depends on built-in type '{dependency.type.__name__}'."
1590
+ )
1591
+
1592
+ # Try to resolve from container using type (Abstract or Interface)
1593
+ if self.bound(dependency.type):
1594
+ return self.resolve(self.getBinding(dependency.type))
1595
+
1596
+ # Try to resolve from container using full class path
1597
+ if self.bound(dependency.full_class_path):
1598
+ return self.resolve(self.getBinding(dependency.full_class_path))
1599
+
1600
+ # Try auto-resolution first
1601
+ if self.__canAutoResolve(dependency.type):
1602
+ return self.__autoResolve(dependency.type)
1603
+
1604
+ # Try to instantiate directly if it's a concrete class
1605
+ if ReflectionConcrete.isConcreteClass(dependency.type):
1606
+ return self.__instantiateWithReflection(dependency.type, is_class=True)
1607
+
1608
+ # Try to call directly if it's a callable
1609
+ if callable(dependency.type) and not isinstance(dependency.type, type):
1610
+ return self.__instantiateWithReflection(dependency.type, is_class=False)
1611
+
1612
+ # If the dependency cannot be resolved, raise an exception
1613
+ raise OrionisContainerException(
1614
+ f"Cannot resolve dependency '{param_name}' of type '{dependency.type.__name__}' for '{name}'."
1615
+ )
1616
+
1617
+ def __instantiateWithReflection(
1618
+ self,
1619
+ target: Callable[..., Any],
1620
+ is_class: bool = True
1621
+ ) -> Any:
1622
+ """
1623
+ Helper method to instantiate a target with reflection-based dependency resolution.
1624
+
1625
+ Parameters
1626
+ ----------
1627
+ target : Callable[..., Any]
1628
+ The class or callable to instantiate.
1629
+ is_class : bool
1630
+ Whether the target is a class (True) or callable (False).
1631
+
1632
+ Returns
1633
+ -------
1634
+ Any
1635
+ The instantiated object or callable result.
1636
+ """
1637
+
1638
+ resolved_params = self.__resolveDependencies(
1639
+ *self.__reflectTarget(target, is_class=is_class)
1640
+ )
1641
+ return target(**resolved_params)
1642
+
1643
+ def resolveWithoutContainer(
1644
+ self,
1645
+ type_: Callable[..., Any],
1646
+ *args,
1647
+ **kwargs
1648
+ ) -> Any:
1649
+ """
1650
+ Forces resolution of a type whether it's registered in the container or not.
1651
+
1652
+ Parameters
1653
+ ----------
1654
+ type_ : Callable[..., Any]
1655
+ The type or callable to resolve.
1656
+ *args : tuple
1657
+ Positional arguments to pass to the constructor/callable.
1658
+ **kwargs : dict
1659
+ Keyword arguments to pass to the constructor/callable.
1660
+
1661
+ Returns
1662
+ -------
1663
+ Any
1664
+ The resolved instance.
1665
+
1666
+ Raises
1667
+ ------
1668
+ OrionisContainerException
1669
+ If the type cannot be resolved.
1670
+ """
1671
+ try:
1672
+ # If args or kwargs are provided, use them directly
1673
+ if args or kwargs:
1674
+ if isinstance(type_, type):
1675
+ return type_(*args, **kwargs)
1676
+ elif callable(type_):
1677
+ return type_(*args, **kwargs)
1678
+
1679
+ # Try auto-resolution first for unregistered types
1680
+ if self.__canAutoResolve(type_):
1681
+ return self.__autoResolve(type_)
1682
+
1683
+ # Use the unified reflection-based instantiation
1684
+ if ReflectionConcrete.isConcreteClass(type_):
1685
+ return self.__instantiateWithReflection(type_, is_class=True)
1686
+
1687
+ if callable(type_) and not isinstance(type_, type):
1688
+ return self.__instantiateWithReflection(type_, is_class=False)
1689
+
1690
+ # If the type is neither a concrete class nor a callable, raise an exception
1691
+ raise OrionisContainerException(
1692
+ f"Cannot force resolve: {getattr(type_, '__name__', str(type_))} is neither a concrete class nor a callable."
1693
+ )
1694
+
1695
+ except Exception as e:
1696
+ if isinstance(e, OrionisContainerException):
1697
+ raise e from None
1698
+
1699
+ raise OrionisContainerException(
1700
+ f"Error resolving '{getattr(type_, '__name__', str(type_))}': {str(e)}"
1701
+ ) from e
1702
+
1703
+ def __canAutoResolve(
1704
+ self,
1705
+ type_: Callable[..., Any]
1706
+ ) -> bool:
1707
+ """
1708
+ Check if a type can be automatically resolved by the container.
1709
+
1710
+ This method determines whether a given type meets all the criteria for
1711
+ automatic dependency resolution. For a type to be auto-resolvable, it must
1712
+ be a concrete class that belongs to a valid application namespace, is
1713
+ not a built-in Python type, and is actually instantiable.
1714
+
1715
+ The validation process includes checking that the type is:
1716
+ - A proper class (not a function, module, or other callable)
1717
+ - Not a built-in Python type (str, int, list, etc.)
1718
+ - A concrete class (not abstract or interface)
1719
+ - Not a generic type or type variable
1720
+ - Part of a valid namespace defined in the container configuration
1721
+ - Actually instantiable (has a callable constructor)
1722
+
1723
+ Parameters
1724
+ ----------
1725
+ type_ : Callable[..., Any]
1726
+ The type to check for auto-resolution eligibility. This should be
1727
+ a class or callable that represents the type to be validated for
1728
+ automatic dependency injection.
1729
+
1730
+ Returns
1731
+ -------
1732
+ bool
1733
+ True if the type can be automatically resolved by the container,
1734
+ False otherwise. Returns True only when all validation criteria
1735
+ are met: the type is a concrete, instantiable class from a valid
1736
+ namespace and is not a built-in type.
1737
+ """
1738
+
1739
+ # Check if the provided parameter is actually a class type
1740
+ # Functions, modules, and other callables are not eligible for auto-resolution
1741
+ if not isinstance(type_, type):
1742
+ return False
1743
+
1744
+ # Exclude built-in Python types (str, int, list, dict, etc.)
1745
+ # These types cannot be auto-resolved as they require explicit values
1746
+ if hasattr(type_, '__module__') and type_.__module__ == 'builtins':
1747
+ return False
1748
+
1749
+ # Check if the type belongs to a valid namespace for auto-resolution
1750
+ # Only classes from configured application namespaces are allowed
1751
+ if not self.__isValidNamespace(type_):
1752
+ return False
1753
+
1754
+ # Check if the type is actually instantiable
1755
+ if not self.__isInstantiable(type_):
1756
+ return False
1757
+
1758
+ # All checks passed - type can be auto-resolved
1759
+ return True
1760
+
1761
+ def __isValidNamespace(self, type_: type) -> bool:
1762
+ """
1763
+ Checks if a type belongs to a valid namespace for auto-resolution.
1764
+
1765
+ Parameters
1766
+ ----------
1767
+ type_ : type
1768
+ The type to check for valid namespace.
1769
+
1770
+ Returns
1771
+ -------
1772
+ bool
1773
+ True if the type belongs to a valid namespace, False otherwise.
1774
+ """
1775
+
1776
+ # Verify that the module name starts with any of the valid namespace prefixes
1777
+ if hasattr(type_, '__module__'):
1778
+ module_name = type_.__module__
1779
+ return any(module_name.startswith(namespace) for namespace in self.__valid_namespaces)
1780
+
1781
+ # If the type has no module information, it cannot be auto-resolved
1782
+ return False
1783
+
1784
+ def __isInstantiable(self, type_: type) -> bool:
1785
+ """
1786
+ Checks if a type is actually instantiable (not abstract, not generic, etc.).
1787
+
1788
+ This method performs comprehensive checks to determine if a class can be
1789
+ safely instantiated through auto-resolution. It validates that the class
1790
+ is concrete, not abstract, not a generic type, and has a callable constructor.
1791
+
1792
+ Parameters
1793
+ ----------
1794
+ type_ : type
1795
+ The type to check for instantiability.
1796
+
1797
+ Returns
1798
+ -------
1799
+ bool
1800
+ True if the type can be instantiated, False otherwise.
1801
+ """
1802
+
1803
+ try:
1804
+
1805
+ # 1. Check if it's a concrete class using ReflectionConcrete
1806
+ if not ReflectionConcrete.isConcreteClass(type_):
1807
+ return False
1808
+
1809
+ # 2. Check if it's an abstract class or has abstract methods
1810
+ if self.__isAbstractClass(type_):
1811
+ return False
1812
+
1813
+ # 3. Check if it's a generic type (like List[T], Dict[K,V])
1814
+ if self.__isGenericType(type_):
1815
+ return False
1816
+
1817
+ # 4. Check if it's a protocol or typing construct
1818
+ if self.__isProtocolOrTyping(type_):
1819
+ return False
1820
+
1821
+ # 5. Check if the constructor is callable
1822
+ if not hasattr(type_, '__init__'):
1823
+ return False
1824
+
1825
+ # 6. Basic instantiation test with empty args (if no required params)
1826
+ if self.__hasRequiredConstructorParams(type_):
1827
+ # If it has required params, we can still auto-resolve if they can be resolved
1828
+ return True
1829
+ else:
1830
+ # If no required params, try a quick instantiation test
1831
+ return self.__canQuickInstantiate(type_)
1832
+
1833
+ except Exception:
1834
+ # If any check fails with an exception, consider it non-instantiable
1835
+ return False
1836
+
1837
+ def __isAbstractClass(self, type_: type) -> bool:
1838
+ """
1839
+ Checks if a type is an abstract class.
1840
+
1841
+ Parameters
1842
+ ----------
1843
+ type_ : type
1844
+ The type to check.
1845
+
1846
+ Returns
1847
+ -------
1848
+ bool
1849
+ True if the type is abstract, False otherwise.
1850
+ """
1851
+
1852
+ # Check if it's explicitly marked as abstract
1853
+ if hasattr(type_, '__abstractmethods__') and type_.__abstractmethods__:
1854
+ return True
1855
+
1856
+ # Check if it inherits from ABC (safely)
1857
+ try:
1858
+ if issubclass(type_, abc.ABC):
1859
+ return True
1860
+ except TypeError:
1861
+ # type_ is not a class, so it can't be abstract
1862
+ pass
1863
+
1864
+ # Check if it has abstract methods
1865
+ try:
1866
+ # Try to get abstract methods using reflection
1867
+ for attr_name in dir(type_):
1868
+ attr = getattr(type_, attr_name)
1869
+ if hasattr(attr, '__isabstractmethod__') and attr.__isabstractmethod__:
1870
+ return True
1871
+ except Exception:
1872
+ pass
1873
+
1874
+ return False
1875
+
1876
+ def __isGenericType(self, type_: type) -> bool:
1877
+ """
1878
+ Checks if a type is a generic type (e.g., List[T], Dict[K,V]).
1879
+
1880
+ Parameters
1881
+ ----------
1882
+ type_ : type
1883
+ The type to check.
1884
+
1885
+ Returns
1886
+ -------
1887
+ bool
1888
+ True if the type is generic, False otherwise.
1889
+ """
1890
+
1891
+ # Check for generic alias (Python 3.7+)
1892
+ if hasattr(typing, 'get_origin') and typing.get_origin(type_) is not None:
1893
+ return True
1894
+
1895
+ # Check for older style generic types
1896
+ if hasattr(type_, '__origin__'):
1897
+ return True
1898
+
1899
+ # Check if it's a typing construct
1900
+ if hasattr(typing, '_GenericAlias') and isinstance(type_, typing._GenericAlias):
1901
+ return True
1902
+
1903
+ # Check for type variables
1904
+ if hasattr(typing, 'TypeVar') and isinstance(type_, typing.TypeVar):
1905
+ return True
1906
+
1907
+ return False
1908
+
1909
+ def __isProtocolOrTyping(self, type_: type) -> bool:
1910
+ """
1911
+ Checks if a type is a Protocol or other typing construct that shouldn't be instantiated.
1912
+
1913
+ Parameters
1914
+ ----------
1915
+ type_ : type
1916
+ The type to check.
1917
+
1918
+ Returns
1919
+ -------
1920
+ bool
1921
+ True if the type is a protocol or typing construct, False otherwise.
1922
+ """
1923
+
1924
+ # Check if it's a Protocol (Python 3.8+)
1925
+ try:
1926
+ if hasattr(typing, 'Protocol') and issubclass(type_, typing.Protocol):
1927
+ return True
1928
+
1929
+ # type_ is not a class, so it can't be a Protocol
1930
+ except TypeError:
1931
+ pass
1932
+
1933
+ # Check if it's in the typing module
1934
+ if hasattr(type_, '__module__') and type_.__module__ == 'typing':
1935
+ return True
1936
+
1937
+ # Check for common typing constructs that shouldn't be instantiated
1938
+ typing_constructs = ['Union', 'Optional', 'Any', 'Callable', 'Type']
1939
+ if hasattr(type_, '__name__') and type_.__name__ in typing_constructs:
1940
+ return True
1941
+
1942
+ return False
1943
+
1944
+ def __hasRequiredConstructorParams(self, type_: type) -> bool:
1945
+ """
1946
+ Checks if a type has required constructor parameters.
1947
+
1948
+ Parameters
1949
+ ----------
1950
+ type_ : type
1951
+ The type to check.
1952
+
1953
+ Returns
1954
+ -------
1955
+ bool
1956
+ True if the type has required constructor parameters, False otherwise.
1957
+ """
1958
+
1959
+ try:
1960
+
1961
+ # Use reflection to get constructor dependencies
1962
+ reflection = ReflectionConcrete(type_)
1963
+ dependencies = reflection.getConstructorDependencies()
1964
+
1965
+ # Check if there are any unresolved dependencies or required parameters
1966
+ if dependencies and dependencies.unresolved:
1967
+ return True
1968
+
1969
+ # Check if there are resolved dependencies that don't have defaults and can't be resolved
1970
+ if dependencies and dependencies.resolved:
1971
+ for param_name, dep in dependencies.resolved.items():
1972
+ # Only consider it required if it has no default AND can't be resolved by container
1973
+ if dep.default is None and not self.bound(dep.type) and not self.bound(dep.full_class_path):
1974
+ return True
1975
+
1976
+ # If no unresolved dependencies and all resolved have defaults, return False
1977
+ return False
1978
+
1979
+ except Exception:
1980
+
1981
+ # If reflection fails, assume it has required params to be safe
1982
+ return True
1983
+
1984
+ def __canQuickInstantiate(self, type_: type) -> bool:
1985
+ """
1986
+ Performs a quick instantiation test to verify the type can be created.
1987
+
1988
+ This method attempts to create an instance of the type without arguments
1989
+ to verify that it can be instantiated successfully.
1990
+
1991
+ Parameters
1992
+ ----------
1993
+ type_ : type
1994
+ The type to test.
1995
+
1996
+ Returns
1997
+ -------
1998
+ bool
1999
+ True if the type can be instantiated, False otherwise.
2000
+ """
2001
+
2002
+ try:
2003
+ # For safety, first check if the constructor signature suggests it's safe to instantiate
2004
+ try:
2005
+
2006
+ # Use inspect to get the constructor signature
2007
+ sig = inspect.signature(type_.__init__)
2008
+
2009
+ # If __init__ has required parameters beyond 'self', skip quick instantiation
2010
+ required_params = [
2011
+ p for name, p in sig.parameters.items()
2012
+ if name != 'self' and p.default == inspect.Parameter.empty
2013
+ ]
2014
+
2015
+ # If there are required parameters, we cannot quick instantiate
2016
+ if required_params:
2017
+ return False
2018
+
2019
+ except (ValueError, TypeError):
2020
+
2021
+ # If we can't inspect the signature, assume it's not safe
2022
+ return False
2023
+
2024
+ # Attempt to create an instance only if it seems safe
2025
+ instance = type_()
2026
+
2027
+ # If successful, clean up and return True
2028
+ del instance
2029
+ return True
2030
+
2031
+ except Exception:
2032
+
2033
+ # If instantiation fails for any reason, it's not auto-resolvable
2034
+ return False
2035
+
2036
+ def __autoResolve(
2037
+ self,
2038
+ type_: Callable[..., Any],
2039
+ *args,
2040
+ **kwargs
2041
+ ) -> Any:
2042
+ """
2043
+ Automatically resolves and instantiates a type with its dependencies.
2044
+
2045
+ Parameters
2046
+ ----------
2047
+ type_ : Callable[..., Any]
2048
+ The class or callable to auto-resolve.
2049
+ *args : tuple
2050
+ Positional arguments to pass directly to the constructor or callable.
2051
+ **kwargs : dict
2052
+ Keyword arguments to pass directly to the constructor or callable.
2053
+
2054
+ Returns
2055
+ -------
2056
+ Any
2057
+ An instance of the requested type, with all dependencies resolved recursively.
2058
+
2059
+ Raises
2060
+ ------
2061
+ OrionisContainerException
2062
+ If the type cannot be auto-resolved or if circular dependencies are detected.
2063
+ """
2064
+
2065
+ # Build a unique key for the type to track resolution and detect circular dependencies
2066
+ type_key = f"{type_.__module__}.{type_.__name__}"
2067
+ if type_key in self.__resolution_cache:
2068
+ raise OrionisContainerException(
2069
+ f"Circular dependency detected while auto-resolving '{type_.__name__}'"
2070
+ )
2071
+
2072
+ try:
2073
+ # Mark this type as being resolved to prevent circular dependencies
2074
+ self.__resolution_cache[type_key] = True
2075
+
2076
+ # Validate that the type can still be auto-resolved at resolution time
2077
+ if not self.__canAutoResolve(type_):
2078
+ raise OrionisContainerException(
2079
+ f"Type '{type_.__name__}' cannot be auto-resolved. "
2080
+ f"It may be abstract, generic, or not from a valid namespace."
2081
+ )
2082
+
2083
+ # If explicit arguments are provided, instantiate/call directly
2084
+ if args or kwargs:
2085
+ return type_(*args, **kwargs)
2086
+
2087
+ # Use unified reflection-based instantiation
2088
+ if ReflectionConcrete.isConcreteClass(type_):
2089
+ return self.__instantiateWithReflection(type_, is_class=True)
2090
+ elif callable(type_) and not isinstance(type_, type):
2091
+ return self.__instantiateWithReflection(type_, is_class=False)
2092
+ else:
2093
+ raise OrionisContainerException(
2094
+ f"Type '{type_.__name__}' is not a concrete class or callable"
2095
+ )
2096
+
2097
+ except Exception as e:
2098
+ # Remove the type from the resolution cache on error
2099
+ self.__resolution_cache.pop(type_key, None)
2100
+
2101
+ if isinstance(e, OrionisContainerException):
2102
+ raise
2103
+ else:
2104
+ raise OrionisContainerException(
2105
+ f"Failed to auto-resolve '{type_.__name__}': {str(e)}"
2106
+ ) from e
2107
+
2108
+ finally:
2109
+ # Always clean up the resolution cache after resolution attempt
2110
+ self.__resolution_cache.pop(type_key, None)
2111
+
2112
+ def call(
2113
+ self,
2114
+ instance: Any,
2115
+ method_name: str,
2116
+ *args,
2117
+ **kwargs
2118
+ ) -> Any:
2119
+ """
2120
+ Call a method on an instance with automatic dependency injection.
2121
+
2122
+ Parameters
2123
+ ----------
2124
+ instance : Any
2125
+ The instance on which to call the method.
2126
+ method_name : str
2127
+ The name of the method to call.
2128
+ *args : tuple
2129
+ Positional arguments to pass to the method.
2130
+ **kwargs : dict
2131
+ Keyword arguments to pass to the method.
2132
+
2133
+ Returns
2134
+ -------
2135
+ Any
2136
+ The result of the method call.
2137
+ """
2138
+
2139
+ # Ensure the instance is a valid object (allow __main__ for development)
2140
+ if instance is None:
2141
+ raise OrionisContainerException("Instance cannot be None")
2142
+
2143
+ if not hasattr(instance, '__class__'):
2144
+ raise OrionisContainerException("Instance must be a valid object with a class")
2145
+
2146
+ # Validate method_name parameter
2147
+ if not isinstance(method_name, str):
2148
+ raise OrionisContainerException(
2149
+ f"Method name must be a string, got {type(method_name).__name__}"
2150
+ )
2151
+
2152
+ if not method_name.strip():
2153
+ raise OrionisContainerException(
2154
+ "Method name cannot be empty or whitespace"
2155
+ )
2156
+
2157
+ # Ensure the method exists and is callable
2158
+ method = getattr(instance, method_name, None)
2159
+ if not callable(method):
2160
+ raise OrionisContainerException(
2161
+ f"Method '{method_name}' not found or not callable on instance '{type(instance).__name__}'."
2162
+ )
2163
+
2164
+ # If args or kwargs are provided, use them directly
2165
+ if args or kwargs:
2166
+ return self.__instantiateCallableWithArgs(method, *args, **kwargs)
2167
+
2168
+ # For methods without provided arguments, try simple call first
2169
+ # This avoids reflection issues for simple methods
2170
+ try:
2171
+
2172
+ # Get method signature to check if it needs parameters
2173
+ sig = inspect.signature(method)
2174
+
2175
+ # Filter out 'self' parameter for bound methods
2176
+ params = [p for name, p in sig.parameters.items()
2177
+ if name != 'self' and p.default == inspect.Parameter.empty]
2178
+
2179
+ # If no required parameters, call directly
2180
+ if not params:
2181
+ return method()
2182
+
2183
+ # If has required parameters, try dependency injection
2184
+ return self.__instantiateWithReflection(method, is_class=False)
2185
+
2186
+ except Exception as reflection_error:
2187
+
2188
+ # If reflection fails, try simple call as fallback
2189
+ try:
2190
+ return method()
2191
+
2192
+ # If simple call also fails, raise the original reflection error
2193
+ except TypeError:
2194
+ raise OrionisContainerException(
2195
+ f"Failed to call method '{method_name}': {reflection_error}"
2196
+ ) from reflection_error