diff --git a/CHANGELOG.md b/CHANGELOG.md index c58ee31..c4b8c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [v1.1.1] - 2026-02-20 -## [v1.0.0] - 2026-02- +### Added + +- Global registry for managing and validating injectables and providers + +### Changed + +- Updated documentation and examples to reflect recent changes +- Injectables have been extracted from Injection and now are handled by Providers +- Refactored resolution logic to use a registry for better management and validation +- On components, products declaration has been removed, use imports declaration + +## [v1.1.0] - 2026-02-13 + +### Fixed + +- Fixed Products not being exported from the package + +## [v1.0.0] - 2026-02-13 ### Added diff --git a/README.md b/README.md index 4827e14..447190b 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,8 @@ from ...plugin...other_component import OtherService from ...plugin...........product import SomeProduct @instance( - imports=[OtherService, ...], # List of dependencies (components) that are needed - products=[SomeProduct, ...], # List of products that this instance will create - provider=providers.Singleton, # Provider type (Singleton, Factory, Resource) + imports=[OtherService, ...], # List of dependencies (components) that this product needs + provider=providers.Singleton, # Provider type from di (Singleton, Factory, Resource) bootstrap=False, # Whether to bootstrap on application start ) class ImplementedSomeService(SomeService): @@ -183,8 +182,7 @@ from ...plugin.....other_product import OtherProduct @product( module=SomeModule, # Declares the module or plugin this component belongs to - imports=[SomeService, ...], # List of dependencies (components) that are needed - products=[OtherProduct, ...], # List of products that this product will create + imports=[SomeService, ...], # List of dependencies (components) that this product needs provider=providers.Singleton, # Provider type (Singleton, Factory, Resource) ) class SomeProduct(Interface, Product): @@ -230,7 +228,6 @@ Some planned features are: - Enhance documentation and examples for better understanding - Implement framework API and extension points for customization - Improve injection resolution and initialization process -- Provide injection scopes and strategies for flexibility - Testing framework integration for better test coverage - Visualization tools for dependency graphs and relationships diff --git a/pyproject.toml b/pyproject.toml index 8db72c9..4ba6a7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ testpaths = ["tests"] [project] name = "module_dependency" -version = "1.1.0" +version = "1.1.1" dependencies = [ "dependency_injector", "jinja2", diff --git a/src/dependency/core/__init__.py b/src/dependency/core/__init__.py index 9a40a3f..3e18578 100644 --- a/src/dependency/core/__init__.py +++ b/src/dependency/core/__init__.py @@ -1,3 +1,15 @@ +from dependency.core.resolution import ( + Container, + Registry, + InjectionResolver, + ResolutionConfig, + ResolutionStrategy, +) +from dependency.core.exceptions import ( + DependencyError, + CancelInitialization, +) + from dependency.core.agrupation import ( Entrypoint, Module, @@ -13,18 +25,16 @@ instance, providers, ) -from dependency.core.resolution import ( - Container, - InjectionResolver, - ResolutionConfig, - ResolutionStrategy, -) -from dependency.core.exceptions import ( - DependencyError, - CancelInitialization, -) + __all__ = [ + "Container", + "Registry", + "InjectionResolver", + "ResolutionConfig", + "ResolutionStrategy", + "DependencyError", + "CancelInitialization", "Entrypoint", "Module", "module", @@ -36,10 +46,4 @@ "product", "instance", "providers", - "Container", - "InjectionResolver", - "ResolutionConfig", - "ResolutionStrategy", - "DependencyError", - "CancelInitialization", ] diff --git a/src/dependency/core/agrupation/entrypoint.py b/src/dependency/core/agrupation/entrypoint.py index 536da80..1c96171 100644 --- a/src/dependency/core/agrupation/entrypoint.py +++ b/src/dependency/core/agrupation/entrypoint.py @@ -3,9 +3,9 @@ from threading import Event from typing import Iterable from dependency.core.agrupation.plugin import Plugin -from dependency.core.injection.injectable import Injectable from dependency.core.resolution.container import Container from dependency.core.resolution.resolver import InjectionResolver +from dependency.core.resolution.strategy import ResolutionStrategy _logger = logging.getLogger("dependency.loader") class Entrypoint: @@ -14,25 +14,21 @@ class Entrypoint: Attributes: init_time (float): Time when the entrypoint was initialized. """ - init_time: float = time.time() - def __init__(self, container: Container, - plugins: Iterable[type[Plugin]] + plugins: Iterable[type[Plugin]], + strategy: ResolutionStrategy = ResolutionStrategy() ) -> None: - providers: list[Injectable] = [] - - for plugin in plugins: - plugin.resolve_container(container=container) - providers.extend(plugin.resolve_providers()) + init_time: float = time.time() self.resolver: InjectionResolver = InjectionResolver( container=container, - providers=providers ) - - self.resolver.resolve_dependencies() - _logger.info(f"Application started in {time.time() - self.init_time} seconds") + self.resolver.resolve_dependencies( + modules=plugins, + strategy=strategy, + ) + _logger.info(f"Application started in {time.time() - init_time} seconds") def main_loop(self) -> None: """Main loop for the application. Waits indefinitely.""" diff --git a/src/dependency/core/agrupation/plugin.py b/src/dependency/core/agrupation/plugin.py index 7508663..59fe49a 100644 --- a/src/dependency/core/agrupation/plugin.py +++ b/src/dependency/core/agrupation/plugin.py @@ -32,6 +32,12 @@ class Plugin(Module): def on_declaration(cls) -> None: cls.injection.is_root = True + @classmethod + def on_resolution(cls, + container: Container + ) -> None: + cls.resolve_container(container=container) + @classmethod def resolve_container(cls, container: Container) -> None: """Resolve the plugin configuration. @@ -43,7 +49,6 @@ def resolve_container(cls, container: Container) -> None: ResolutionError: If the configuration is invalid. """ try: - cls.inject_container(container) config_cls = get_type_hints(cls).get("config", object) if issubclass(config_cls, BaseModel): setattr(cls, "config", config_cls.model_validate(container.config())) diff --git a/src/dependency/core/declaration/component.py b/src/dependency/core/declaration/component.py index b52f031..235d51f 100644 --- a/src/dependency/core/declaration/component.py +++ b/src/dependency/core/declaration/component.py @@ -13,9 +13,8 @@ class Component(ProviderMixin): def component( module: Optional[type[Module]] = None, imports: Iterable[type[ProviderMixin]] = (), - products: Iterable[type[ProviderMixin]] = (), - provider: Optional[InstanceOrClass[providers.Provider[Any]]] = None, partial_resolution: bool = False, + provider: Optional[InstanceOrClass[providers.Provider[Any]]] = None, bootstrap: bool = False, ) -> Callable[[type[COMPONENT]], type[COMPONENT]]: """Decorator for Component class @@ -23,7 +22,6 @@ def component( Args: module (type[Module], optional): Module where the component is registered. Defaults to None. imports (Iterable[type[ProviderMixin]], optional): List of components to be imported by the provider. Defaults to (). - products (Iterable[type[ProviderMixin]], optional): List of products to be declared by the provider. Defaults to (). provider (Optional[providers.Provider[Any]], optional): Provider to be used. Defaults to None. partial_resolution (bool, optional): Whether the component should be resolved with partial resolution. Defaults to False. bootstrap (bool, optional): Whether the provider should be bootstrapped. Defaults to False. @@ -49,9 +47,8 @@ def wrap(cls: type[COMPONENT]) -> type[COMPONENT]: bootstrap=cls.provide if bootstrap else None, ) - cls.set_dependencies( + cls.update_dependencies( imports=imports, - products=products, partial_resolution=partial_resolution, ) diff --git a/src/dependency/core/declaration/instance.py b/src/dependency/core/declaration/instance.py index a513882..8951842 100644 --- a/src/dependency/core/declaration/instance.py +++ b/src/dependency/core/declaration/instance.py @@ -6,7 +6,6 @@ def instance( imports: Iterable[type[ProviderMixin]] = (), - products: Iterable[type[ProviderMixin]] = (), provider: type[providers.Provider[Any]] = providers.Singleton, bootstrap: bool = False, ) -> Callable[[type[COMPONENT]], type[COMPONENT]]: @@ -14,7 +13,6 @@ def instance( Args: imports (Iterable[type[ProviderMixin]], optional): List of components to be imported by the provider. Defaults to (). - products (Iterable[type[ProviderMixin]], optional): List of products to be declared by the provider. Defaults to (). provider (type[providers.Provider[Any]], optional): Provider to be used. Defaults to providers.Singleton. bootstrap (bool, optional): Whether the provider should be bootstrapped. Defaults to False. @@ -34,9 +32,8 @@ def wrap(cls: type[COMPONENT]) -> type[COMPONENT]: bootstrap=cls.provide if bootstrap else None, ) - cls.set_dependencies( + cls.update_dependencies( imports=imports, - products=products, ) return cls diff --git a/src/dependency/core/declaration/product.py b/src/dependency/core/declaration/product.py index 3f22037..9fdbaf6 100644 --- a/src/dependency/core/declaration/product.py +++ b/src/dependency/core/declaration/product.py @@ -11,7 +11,6 @@ class Product(Component): def product( module: Optional[type[Module]] = None, imports: Iterable[type[ProviderMixin]] = (), - products: Iterable[type[ProviderMixin]] = (), provider: type[providers.Provider[Any]] = providers.Factory, partial_resolution: bool = False, bootstrap: bool = False, @@ -35,7 +34,6 @@ def product( return component( module=module, imports=imports, - products=products, provider=provider, partial_resolution=partial_resolution, bootstrap=bootstrap, diff --git a/src/dependency/core/injection/injectable.py b/src/dependency/core/injection/injectable.py index 798722d..8cf03ba 100644 --- a/src/dependency/core/injection/injectable.py +++ b/src/dependency/core/injection/injectable.py @@ -1,11 +1,5 @@ import logging -from typing import Any, Callable, Iterable, Self, Optional -from dependency_injector import containers, providers -from dependency.core.exceptions import ( - DeclarationError, - InitializationError, - CancelInitialization, -) +from typing import Any, Callable, Iterable, Optional _logger = logging.getLogger("dependency.loader") class Injectable: @@ -13,60 +7,69 @@ class Injectable: Attributes: interface_cls (T): The interface class that this injectable implements. - - imports (Iterable[Injectable]): List of injectables that this injectable depends on. - products (Iterable[Injectable]): List of injectables that depend on this injectable. """ def __init__(self, interface_cls: type, + implementation: Optional[type] = None, ) -> None: self.interface_cls: type = interface_cls self.modules_cls: set[type] = {interface_cls} - self.implementation: Optional[type] = None - self.__provider: Optional[providers.Provider[Any]] = None - self.__bootstrap: Optional[Callable[[], Any]] = None + # Implementation details + self.implementation: Optional[type] = implementation + self.bootstrap: Optional[Callable[[], Any]] = None + # Dependency tracking self.imports: set['Injectable'] = set() - self.products: set['Injectable'] = set() - #self.import_of: set['Injectable'] = set() - #self.product_of: set['Injectable'] = set() + self.dependent: set['Injectable'] = set() + # Validation flags self.partial_resolution: bool = False self.is_resolved: bool = False - @property - def import_resolved(self) -> bool: - unresolved: set['Injectable'] = set(filter(lambda i: not i.is_resolved, self.imports)) - if not unresolved: - return True + def check_resolved(self, providers: list['Injectable']) -> bool: + if self.implementation is None: + return False if self.partial_resolution: - _logger.warning(f"Provider {self.interface_cls.__name__} has unresolved imports: {unresolved}, but partial resolution is enabled") - return True + def validation(i: 'Injectable') -> bool: + return ( + i.is_resolved or + i.partial_resolution or + i not in providers + ) + else: + def validation(i: 'Injectable') -> bool: + return i.is_resolved + + for provider in self.imports: + if not validation(provider): + return False - return False + self.is_resolved = True + return True - def add_dependencies(self, + def update_dependencies(self, imports: Iterable['Injectable'], - products: Iterable['Injectable'], - partial_resolution: bool = False, + partial_resolution: Optional[bool] = None, ) -> None: self.imports.update(imports) - self.products.update(products) - self.partial_resolution = partial_resolution + for i in imports: + i.dependent.add(self) + + if partial_resolution is not None: + self.partial_resolution = partial_resolution - def del_dependencies(self, + def discard_dependencies(self, imports: Iterable['Injectable'], - products: Iterable['Injectable'], ) -> None: self.imports.difference_update(imports) - self.products.difference_update(products) + for i in imports: + i.dependent.discard(self) - def add_implementation(self, + def set_implementation(self, implementation: type, modules_cls: Iterable[type], - provider: providers.Provider[Any], bootstrap: Optional[Callable[[], Any]] = None ) -> None: if self.implementation is None: @@ -74,59 +77,9 @@ def add_implementation(self, else: _logger.warning(f"Provider {self.interface_cls.__name__} implementation reassigned: {self.implementation.__name__} -> {implementation.__name__}") - self.modules_cls.update(modules_cls) self.implementation = implementation - self.__provider = provider - self.__bootstrap = bootstrap - - @property - def provider(self) -> providers.Provider[Any]: - """Return the provider instance for this injectable.""" - if self.__provider is None: - raise DeclarationError(f"Provider {self.interface_cls.__name__} has no implementation assigned") - return self.__provider - - @property - def provide(self) -> providers.Provider[Any]: - """Return the provide instance for this injectable.""" - if not self.is_resolved: - raise DeclarationError( - f"Injectable {self.interface_cls.__name__} accessed before being resolved. " - f"Ensure it is declared as a dependency (imports or products) where it is being used" - ) - return self.provider - - def inject(self) -> Self: - """Mark the provider injection as resolved.""" - self.is_resolved = True - return self - - def wire(self, container: containers.DynamicContainer) -> None: - """Wire the provider with the given container. - - Args: - container (containers.DynamicContainer): Container to wire the provider with. - """ - container.wire( - modules=self.modules_cls, - warn_unresolved=True, - ) - - def init(self) -> None: - """Execute the bootstrap function if it exists.""" - if not self.is_resolved: - raise DeclarationError( - f"Injectable {self.interface_cls.__name__} must be resolved before initialization. " - f"Ensure it is declared as a dependency (imports or products) where it is being used" - ) - - if self.__bootstrap is not None: - try: - self.__bootstrap() - except CancelInitialization as e: - _logger.warning(f"Injectable {self.interface_cls.__name__} initialization skipped (cancelled by user): {e}") - except Exception as e: - raise InitializationError(f"Injectable {self.interface_cls.__name__} initialization failed") from e + self.modules_cls.update(modules_cls) + self.bootstrap = bootstrap def __repr__(self) -> str: return f"{self.interface_cls.__name__}" diff --git a/src/dependency/core/injection/injection.py b/src/dependency/core/injection/injection.py index 45a8f40..f00f653 100644 --- a/src/dependency/core/injection/injection.py +++ b/src/dependency/core/injection/injection.py @@ -3,8 +3,7 @@ from typing import Any, Generator, Optional, override from dependency_injector import containers, providers from dependency.core.injection.injectable import Injectable -from dependency.core.injection.wiring import LazyProvide -from dependency.core.exceptions import ProvisionError +from dependency.core.exceptions import DeclarationError, ProvisionError _logger = logging.getLogger("dependency.loader") class BaseInjection(ABC): @@ -39,11 +38,6 @@ def change_parent(self, parent: Optional['ContainerInjection'] = None) -> None: if self.parent is not None: self.parent.childs.add(self) - def validation(self) -> None: - """Validate the injection configuration.""" - if self.parent is None and not self.is_root: - _logger.warning(f"Injection {self.name} has no parent module (consider registering)") - @abstractmethod def inject_cls(self) -> Any: """Return the class to be injected.""" @@ -52,9 +46,6 @@ def inject_cls(self) -> Any: def resolve_providers(self) -> Generator[Injectable, None, None]: """Inject all children into the current injection context.""" - def __hash__(self) -> int: - return hash(self.name) - def __repr__(self) -> str: return self.name @@ -86,11 +77,26 @@ class ProviderInjection(BaseInjection): """ def __init__(self, name: str, - interface_cls: type, - parent: Optional['ContainerInjection'] = None + injectable: Injectable, + parent: Optional['ContainerInjection'] = None, + provider: Optional[providers.Provider[Any]] = None, ) -> None: super().__init__(name=name, parent=parent) - self.injectable: Injectable = Injectable(interface_cls=interface_cls) + self._injectable: Injectable = injectable + self._provider: Optional[providers.Provider[Any]] = provider + + def set_provider(self, provider: providers.Provider[Any]) -> None: + """Set the provider instance for this injectable. + + Args: + provider (providers.Provider[Any]): The provider instance to set. + """ + self._provider = provider + + @property + def injectable(self) -> Injectable: + """Return the injectable instance for this provider.""" + return self._injectable @property @override @@ -102,15 +108,17 @@ def reference(self) -> str: @property def provider(self) -> providers.Provider[Any]: - """Return the provider instance.""" - return LazyProvide[lambda: self.reference] # type: ignore + """Return the provider instance for this injectable.""" + if self._provider is None: + raise DeclarationError(f"Provider {self} has no implementation assigned") + return self._provider @override def inject_cls(self) -> providers.Provider[Any]: """Return the provider instance.""" - return self.injectable.provider + return self.provider @override def resolve_providers(self) -> Generator[Injectable, None, None]: """Inject all imports into the current injectable.""" - yield self.injectable + yield self._injectable diff --git a/src/dependency/core/injection/mixin.py b/src/dependency/core/injection/mixin.py index 5c6da6a..a23f7ae 100644 --- a/src/dependency/core/injection/mixin.py +++ b/src/dependency/core/injection/mixin.py @@ -3,6 +3,8 @@ from dependency.core.injection.injectable import Injectable from dependency.core.injection.injection import ContainerInjection, ProviderInjection from dependency.core.resolution.container import Container +from dependency.core.resolution.registry import Registry +from dependency.core.exceptions import DeclarationError class ContainerMixin: """Container Mixin Class @@ -17,6 +19,16 @@ def on_declaration(cls) -> None: """Hook method called upon declaration of the container. """ + @classmethod + def on_resolution(cls, + container: Container + ) -> None: + """Hook method called upon resolution of the container. + + Args: + container (Container): The application container. + """ + @classmethod def init_injection(cls, parent: Optional[ContainerInjection], @@ -31,7 +43,6 @@ def init_injection(cls, parent=parent, ) cls.on_declaration() - cls.injection.validation() @classmethod def change_parent(cls, parent: Optional['ContainerMixin'] = None) -> None: @@ -50,6 +61,7 @@ def inject_container(cls, container: Container) -> None: container (Container): The application container. """ setattr(container, cls.injection.name, cls.injection.inject_cls()) + cls.on_resolution(container=container) @classmethod def resolve_providers(cls) -> Generator[Injectable, None, None]: @@ -67,6 +79,7 @@ class ProviderMixin: injection (ProviderInjection): Injection handler for the provider """ injection: ProviderInjection + injectable: Injectable @classmethod def on_declaration(cls) -> None: @@ -82,19 +95,22 @@ def init_injection(cls, Args: parent (Optional[ContainerInjection]): Parent container injection instance. """ + cls.injectable = Injectable( + interface_cls=cls + ) cls.injection = ProviderInjection( name=cls.__name__, - interface_cls=cls, + injectable=cls.injectable, parent=parent, ) cls.on_declaration() - cls.injection.validation() + Registry.register(cls.injection) @classmethod def init_implementation(cls, modules_cls: Iterable[type], provider: providers.Provider[Any], - bootstrap: Optional[Callable[[], Any]] + bootstrap: Optional[Callable[[], Any]], ) -> None: """Initialize the injectable for the provider. @@ -106,46 +122,47 @@ def init_implementation(cls, Raises: TypeError: If the class is not a subclass of the interface class. """ - interface_cls: type = cls.injection.injectable.interface_cls + interface_cls: type = cls.injectable.interface_cls if not issubclass(cls, interface_cls): raise TypeError(f"Class {cls.__name__} must be a subclass of {interface_cls.__name__} to be used as an instance of component {cls.__name__}") - cls.injection.injectable.add_implementation( + cls.injection.set_provider( + provider=provider + ) + cls.injectable.set_implementation( implementation=cls, modules_cls=modules_cls, - provider=provider, bootstrap=bootstrap, ) @classmethod - def set_dependencies(cls, + def change_parent(cls, parent: Optional[type['ContainerMixin']] = None) -> None: + """Change the parent injection of this mixin. + """ + cls.injection.change_parent(parent.injection if parent else None) + + @classmethod + def update_dependencies(cls, imports: Iterable[type['ProviderMixin']] = (), - products: Iterable[type['ProviderMixin']] = (), - partial_resolution: bool = False, + partial_resolution: Optional[bool] = None, ) -> None: """Initialize the dependencies for the provider. Args: - imports (Iterable[type["ResolubleClass"]]): List of components to be imported by the provider. - products (Iterable[type["ResolubleClass"]]): List of products to be declared by the provider. + imports (Iterable[type["ResolubleClass"]]): List of providers to be imported by the provider. partial_resolution (bool, optional): Whether to allow partial resolution of dependencies. Defaults to False. """ - cls.injection.injectable.add_dependencies( + cls.injectable.update_dependencies( imports={ - injection.injection.injectable - for injection in imports - }, - products={ - injection.injection.injectable - for injection in products + provider.injectable + for provider in imports }, partial_resolution=partial_resolution, ) @classmethod - def remove_dependencies(cls, + def discard_dependencies(cls, imports: Iterable[type['ProviderMixin']] = (), - products: Iterable[type['ProviderMixin']] = (), ) -> None: """Remove dependencies from the provider. @@ -153,23 +170,13 @@ def remove_dependencies(cls, imports (Iterable[type["ProviderMixin"]]): List of components to remove from imports. products (Iterable[type["ProviderMixin"]]): List of components to remove from products. """ - cls.injection.injectable.del_dependencies( + cls.injectable.discard_dependencies( imports={ - injection.injection.injectable - for injection in imports - }, - products={ - injection.injection.injectable - for injection in products + provider.injectable + for provider in imports }, ) - @classmethod - def change_parent(cls, parent: Optional[type['ContainerMixin']] = None) -> None: - """Change the parent injection of this mixin. - """ - cls.injection.change_parent(parent.injection if parent else None) - @classmethod def reference(cls) -> str: """Return the reference name of the Injectable.""" @@ -178,9 +185,14 @@ def reference(cls) -> str: @classmethod def provider(cls) -> providers.Provider[Any]: """Return the provider instance of the Injectable.""" - return cls.injection.injectable.provider + return cls.injection.provider @classmethod def provide(cls, *args: Any, **kwargs: Any) -> Any: """Provide an instance of the Injectable.""" - return cls.injection.injectable.provide(*args, **kwargs) + if not cls.injectable.is_resolved: + raise DeclarationError( + f"Injectable {cls.injection} accessed before being resolved. " + f"Ensure it is declared as a dependency where it is being used." + ) + return cls.injection.provider(*args, **kwargs) diff --git a/src/dependency/core/resolution/__init__.py b/src/dependency/core/resolution/__init__.py index 1b0e253..25997cc 100644 --- a/src/dependency/core/resolution/__init__.py +++ b/src/dependency/core/resolution/__init__.py @@ -1,9 +1,11 @@ from dependency.core.resolution.container import Container +from dependency.core.resolution.registry import Registry from dependency.core.resolution.resolver import InjectionResolver from dependency.core.resolution.strategy import ResolutionConfig, ResolutionStrategy __all__ = [ "Container", + "Registry", "InjectionResolver", "ResolutionConfig", "ResolutionStrategy", diff --git a/src/dependency/core/resolution/errors.py b/src/dependency/core/resolution/errors.py index a868023..71cb7b7 100644 --- a/src/dependency/core/resolution/errors.py +++ b/src/dependency/core/resolution/errors.py @@ -52,4 +52,4 @@ def raise_resolution_error( circular_error = raise_circular_error(providers) dependency_error = raise_dependency_error(unresolved) if circular_error or dependency_error: - raise ResolutionError("Provider resolution failed due to dependency errors") + raise ResolutionError(f"Provider resolution failed due to dependency errors: {unresolved}") diff --git a/src/dependency/core/resolution/registry.py b/src/dependency/core/resolution/registry.py new file mode 100644 index 0000000..14cf632 --- /dev/null +++ b/src/dependency/core/resolution/registry.py @@ -0,0 +1,19 @@ +import logging +from dependency.core.injection.injectable import Injectable +from dependency.core.injection.injection import ProviderInjection +_logger = logging.getLogger("dependency.loader") + +class Registry: + providers: set[ProviderInjection] = set() + + @classmethod + def register(cls, provider: ProviderInjection) -> None: + cls.providers.add(provider) + + @classmethod + def validation(cls) -> None: + for provider in cls.providers: + if provider.parent is None and not provider.is_root: + _logger.warning(f"Provider {provider} has no parent module (consider registering)") + + injectable: Injectable = provider.injectable diff --git a/src/dependency/core/resolution/resolver.py b/src/dependency/core/resolution/resolver.py index a6d20d1..dba804f 100644 --- a/src/dependency/core/resolution/resolver.py +++ b/src/dependency/core/resolution/resolver.py @@ -1,6 +1,8 @@ from typing import Iterable from dependency.core.injection.injectable import Injectable +from dependency.core.injection.mixin import ContainerMixin from dependency.core.resolution.container import Container +from dependency.core.resolution.registry import Registry from dependency.core.resolution.strategy import ResolutionStrategy class InjectionResolver: @@ -8,22 +10,64 @@ class InjectionResolver: """ def __init__(self, container: Container, - providers: Iterable[Injectable], ) -> None: self.container: Container = container - self.providers: list[Injectable] = list(providers) def resolve_dependencies(self, - strategy: type[ResolutionStrategy] = ResolutionStrategy + modules: Iterable[type[ContainerMixin]], + strategy: ResolutionStrategy = ResolutionStrategy() + ) -> list[Injectable]: + """Resolve dependencies for a list of modules. + + Args: + modules (Iterable[type[ContainerMixin]]): The list of module classes to resolve. + strategy (type[ResolutionStrategy]): The resolution strategy to use. + + Returns: + list[Injectable]: List of resolved injectables. + """ + Registry.validation() + providers = self.resolve_modules( + modules=modules + ) + return self.resolve_providers( + providers=providers, + strategy=strategy + ) + + def resolve_modules(self, + modules: Iterable[type[ContainerMixin]], + ) -> list[Injectable]: + """Resolve all modules and their dependencies. + + Args: + modules (Iterable[type[ContainerMixin]]): The list of module classes to resolve. + + Returns: + list[Injectable]: List of resolved injectables from the modules. + """ + providers: list[Injectable] = [] + + for module in modules: + module.inject_container(container=self.container) + providers.extend(module.resolve_providers()) + + return providers + + def resolve_providers(self, + providers: Iterable[Injectable], + strategy: ResolutionStrategy = ResolutionStrategy() ) -> list[Injectable]: """Resolve all dependencies and initialize them. Args: + providers (Iterable[Injectable]): The list of providers to resolve. strategy (type[ResolutionStrategy]): The resolution strategy to use. Returns: - list[Injectable]: List of resolved injectables.""" + list[Injectable]: List of resolved injectables. + """ return strategy.resolution( container=self.container, - providers=self.providers, + providers=list(providers), ) diff --git a/src/dependency/core/resolution/strategy.py b/src/dependency/core/resolution/strategy.py index 8cc9e7c..460d875 100644 --- a/src/dependency/core/resolution/strategy.py +++ b/src/dependency/core/resolution/strategy.py @@ -1,8 +1,14 @@ import logging from pydantic import BaseModel +from typing import Optional from dependency.core.injection.injectable import Injectable from dependency.core.resolution.container import Container from dependency.core.resolution.errors import raise_resolution_error +from dependency.core.exceptions import ( + DeclarationError, + InitializationError, + CancelInitialization, +) _logger = logging.getLogger("dependency.loader") class ResolutionConfig(BaseModel): @@ -13,105 +19,136 @@ class ResolutionConfig(BaseModel): class ResolutionStrategy: """Defines the strategy for resolving dependencies. """ - config: ResolutionConfig = ResolutionConfig() + def __init__(self, + config: Optional[ResolutionConfig] = None + ) -> None: + self.config: ResolutionConfig = config or ResolutionConfig() - @classmethod - def resolution(cls, - container: Container, + def resolution(self, providers: list[Injectable], + container: Container, ) -> list[Injectable]: """Resolve all dependencies and initialize them. Args: + providers (list[Injectable]): List of providers to resolve. container (Container): The container to wire the injectables with. - providers (list[ProviderInjection]): List of provider injections to resolve. - config (ResolutionConfig): Configuration for the resolution strategy. Returns: list[Injectable]: List of resolved injectables. """ - injectables: list[Injectable] = cls.injection( + providers = self.expand( + providers=providers + ) + self.injection( providers=providers, ) - cls.wiring( + self.wiring( + providers=providers, container=container, - injectables=injectables, ) - cls.initialize( - injectables=injectables + self.initialize( + providers=providers ) - return injectables + return providers - @classmethod - def injection(cls, + def expand(self, providers: list[Injectable], ) -> list[Injectable]: + """Expand the list of providers by adding all their imports. + + Args: + providers (list[Injectable]): List of providers to expand. + + Returns: + list[Injectable]: List of expanded providers. + """ + _logger.info("Expanding dependencies...") + unexpanded: set[Injectable] = set(providers.copy()) + expanded: set[Injectable] = set() + + while unexpanded: + provider: Injectable = unexpanded.pop() + expanded.add(provider) + + if not provider.partial_resolution: + unexpanded.update(filter(lambda i: i not in expanded, provider.imports)) + return list(expanded) + + def injection(self, + providers: list[Injectable], + ) -> None: """Resolve all injectables in layers. Args: providers (list[Injectable]): List of injectables to resolve. Returns: - list[Injectable]: List of resolved injectables. + list[Injectable]: List of unresolved injectables. """ _logger.info("Resolving dependencies...") - unresolved: list[Injectable] = providers.copy() - resolved: list[Injectable] = [] - layer_count: int = 0 + unresolved: set[Injectable] = set(providers.copy()) + resolved: set[Injectable] = set() while unresolved: - new_layer: set[Injectable] = { - provider.inject() - for provider in unresolved - if provider.import_resolved - } + layer_resolved: set[Injectable] = set() + layer_unresolved: set[Injectable] = set() + + for provider in unresolved: + if provider.check_resolved(providers): + layer_resolved.add(provider) + else: + layer_unresolved.add(provider) - if not new_layer: + if not layer_resolved: raise_resolution_error( providers=providers, - unresolved=unresolved + unresolved=list(unresolved), ) - resolved.extend(new_layer) - _logger.debug(f"Layer {layer_count}: {new_layer}") - layer_count += 1 - - for provider in new_layer: - unresolved.extend(provider.products) - - unresolved = [ - provider - for provider in unresolved - if provider not in new_layer - ] - return resolved - - @classmethod - def wiring(cls, + + resolved.update(layer_resolved) + unresolved = layer_unresolved + + def wiring(self, + providers: list[Injectable], container: Container, - injectables: list[Injectable], ) -> None: - """Wire a list of injectables with the given container. + """Wire a list of providers with the given container. Args: - container (Container): The container to wire the injectables with. - injectables (list[Injectable]): List of injectables to wire. + providers (list[Injectable]): List of providers to wire. + container (Container): The container to wire the providers with. """ _logger.info("Wiring dependencies...") - for injectable in injectables: - injectable.wire(container=container) - if cls.config.init_container: + for provider in providers: + container.wire( + modules=provider.modules_cls, + warn_unresolved=True, + ) + if self.config.init_container: container.check_dependencies() container.init_resources() - @classmethod - def initialize(cls, - injectables: list[Injectable], + def initialize(self, + providers: list[Injectable], ) -> None: """Start all implementations by executing their init functions. Args: - injectables (list[Injectable]): List of injectables to start. + providers (list[Injectable]): List of providers to start. """ _logger.info("Initializing dependencies...") - for injectable in injectables: - injectable.init() + for provider in providers: + if not provider.is_resolved: + raise DeclarationError( + f"Injectable {provider} must be resolved before initialization. " + f"Ensure it is declared as a dependency where it is being used" + ) + + if provider.bootstrap is not None: + try: + provider.bootstrap() + except CancelInitialization as e: + _logger.warning(f"Injectable {provider} initialization skipped (cancelled by user): {e}") + except Exception as e: + raise InitializationError(f"Injectable {provider} initialization failed") from e diff --git a/src/dependency/core/utils/cycle.py b/src/dependency/core/utils/cycle.py index 891e614..53b7f2b 100644 --- a/src/dependency/core/utils/cycle.py +++ b/src/dependency/core/utils/cycle.py @@ -65,6 +65,7 @@ def visit(node: T, path: list[T], visited: set[T]) -> None: path.append(node) for dep in function(node): visit(dep, path, visited) + path.pop() for element in elements: visit(element, [], set()) diff --git a/src/example/plugin/hardware/factory/providers/creatorA.py b/src/example/plugin/hardware/factory/providers/creatorA.py index e3cdbd4..10ca705 100644 --- a/src/example/plugin/hardware/factory/providers/creatorA.py +++ b/src/example/plugin/hardware/factory/providers/creatorA.py @@ -9,8 +9,6 @@ @instance( imports=[ HardwareObserver, - ], - products=[ HardwareA, HardwareB, ], diff --git a/src/example/plugin/hardware/factory/providers/creatorB.py b/src/example/plugin/hardware/factory/providers/creatorB.py index 0773359..9f1d61d 100644 --- a/src/example/plugin/hardware/factory/providers/creatorB.py +++ b/src/example/plugin/hardware/factory/providers/creatorB.py @@ -9,10 +9,8 @@ @instance( imports=[ HardwareObserver, - ], - products=[ HardwareB, - HardwareC + HardwareC, ], provider=providers.Singleton, ) diff --git a/src/example/plugin/reporter/facade/facadeA.py b/src/example/plugin/reporter/facade/facadeA.py index a850716..f2cdee3 100644 --- a/src/example/plugin/reporter/facade/facadeA.py +++ b/src/example/plugin/reporter/facade/facadeA.py @@ -9,8 +9,6 @@ @instance( imports=[ HardwareAbstraction, - ], - products=[ ReporterFactory, ], provider=providers.Singleton, diff --git a/src/example/plugin/reporter/factory/__init__.py b/src/example/plugin/reporter/factory/__init__.py index 4345ca8..3fd1427 100644 --- a/src/example/plugin/reporter/factory/__init__.py +++ b/src/example/plugin/reporter/factory/__init__.py @@ -5,7 +5,7 @@ @component( module=ReporterPlugin, - products=[ + imports=[ ReporterA, ], provider=providers.Singleton, diff --git a/stubs/dependency/core/__init__.pyi b/stubs/dependency/core/__init__.pyi index 0b9d749..1b838c5 100644 --- a/stubs/dependency/core/__init__.pyi +++ b/stubs/dependency/core/__init__.pyi @@ -1,6 +1,6 @@ from dependency.core.agrupation import Entrypoint as Entrypoint, Module as Module, Plugin as Plugin, PluginMeta as PluginMeta, module as module from dependency.core.declaration import Component as Component, Product as Product, component as component, instance as instance, product as product, providers as providers from dependency.core.exceptions import CancelInitialization as CancelInitialization, DependencyError as DependencyError -from dependency.core.resolution import Container as Container, InjectionResolver as InjectionResolver, ResolutionConfig as ResolutionConfig, ResolutionStrategy as ResolutionStrategy +from dependency.core.resolution import Container as Container, InjectionResolver as InjectionResolver, Registry as Registry, ResolutionConfig as ResolutionConfig, ResolutionStrategy as ResolutionStrategy -__all__ = ['Entrypoint', 'Module', 'module', 'Plugin', 'PluginMeta', 'Component', 'component', 'Product', 'product', 'instance', 'providers', 'Container', 'InjectionResolver', 'ResolutionConfig', 'ResolutionStrategy', 'DependencyError', 'CancelInitialization'] +__all__ = ['Container', 'Registry', 'InjectionResolver', 'ResolutionConfig', 'ResolutionStrategy', 'DependencyError', 'CancelInitialization', 'Entrypoint', 'Module', 'module', 'Plugin', 'PluginMeta', 'Component', 'component', 'Product', 'product', 'instance', 'providers'] diff --git a/stubs/dependency/core/agrupation/entrypoint.pyi b/stubs/dependency/core/agrupation/entrypoint.pyi index 0c2a29e..14ac702 100644 --- a/stubs/dependency/core/agrupation/entrypoint.pyi +++ b/stubs/dependency/core/agrupation/entrypoint.pyi @@ -1,7 +1,7 @@ from dependency.core.agrupation.plugin import Plugin as Plugin -from dependency.core.injection.injectable import Injectable as Injectable from dependency.core.resolution.container import Container as Container from dependency.core.resolution.resolver import InjectionResolver as InjectionResolver +from dependency.core.resolution.strategy import ResolutionStrategy as ResolutionStrategy from typing import Iterable class Entrypoint: @@ -10,8 +10,7 @@ class Entrypoint: Attributes: init_time (float): Time when the entrypoint was initialized. """ - init_time: float resolver: InjectionResolver - def __init__(self, container: Container, plugins: Iterable[type[Plugin]]) -> None: ... + def __init__(self, container: Container, plugins: Iterable[type[Plugin]], strategy: ResolutionStrategy = ...) -> None: ... def main_loop(self) -> None: """Main loop for the application. Waits indefinitely.""" diff --git a/stubs/dependency/core/agrupation/plugin.pyi b/stubs/dependency/core/agrupation/plugin.pyi index 92c636f..f928b75 100644 --- a/stubs/dependency/core/agrupation/plugin.pyi +++ b/stubs/dependency/core/agrupation/plugin.pyi @@ -24,6 +24,8 @@ class Plugin(Module): @classmethod def on_declaration(cls) -> None: ... @classmethod + def on_resolution(cls, container: Container) -> None: ... + @classmethod def resolve_container(cls, container: Container) -> None: """Resolve the plugin configuration. diff --git a/stubs/dependency/core/declaration/component.pyi b/stubs/dependency/core/declaration/component.pyi index fad796b..6159ff9 100644 --- a/stubs/dependency/core/declaration/component.pyi +++ b/stubs/dependency/core/declaration/component.pyi @@ -10,13 +10,12 @@ class Component(ProviderMixin): """Component Base Class """ -def component(module: type[Module] | None = None, imports: Iterable[type[ProviderMixin]] = (), products: Iterable[type[ProviderMixin]] = (), provider: InstanceOrClass[providers.Provider[Any]] | None = None, partial_resolution: bool = False, bootstrap: bool = False) -> Callable[[type[COMPONENT]], type[COMPONENT]]: +def component(module: type[Module] | None = None, imports: Iterable[type[ProviderMixin]] = (), partial_resolution: bool = False, provider: InstanceOrClass[providers.Provider[Any]] | None = None, bootstrap: bool = False) -> Callable[[type[COMPONENT]], type[COMPONENT]]: """Decorator for Component class Args: module (type[Module], optional): Module where the component is registered. Defaults to None. imports (Iterable[type[ProviderMixin]], optional): List of components to be imported by the provider. Defaults to (). - products (Iterable[type[ProviderMixin]], optional): List of products to be declared by the provider. Defaults to (). provider (Optional[providers.Provider[Any]], optional): Provider to be used. Defaults to None. partial_resolution (bool, optional): Whether the component should be resolved with partial resolution. Defaults to False. bootstrap (bool, optional): Whether the provider should be bootstrapped. Defaults to False. diff --git a/stubs/dependency/core/declaration/instance.pyi b/stubs/dependency/core/declaration/instance.pyi index 6834d90..2b416f9 100644 --- a/stubs/dependency/core/declaration/instance.pyi +++ b/stubs/dependency/core/declaration/instance.pyi @@ -4,12 +4,11 @@ from dependency.core.injection.mixin import ProviderMixin as ProviderMixin from dependency_injector import providers from typing import Any, Callable, Iterable -def instance(imports: Iterable[type[ProviderMixin]] = (), products: Iterable[type[ProviderMixin]] = (), provider: type[providers.Provider[Any]] = ..., bootstrap: bool = False) -> Callable[[type[COMPONENT]], type[COMPONENT]]: +def instance(imports: Iterable[type[ProviderMixin]] = (), provider: type[providers.Provider[Any]] = ..., bootstrap: bool = False) -> Callable[[type[COMPONENT]], type[COMPONENT]]: """Decorator for instance class Args: imports (Iterable[type[ProviderMixin]], optional): List of components to be imported by the provider. Defaults to (). - products (Iterable[type[ProviderMixin]], optional): List of products to be declared by the provider. Defaults to (). provider (type[providers.Provider[Any]], optional): Provider to be used. Defaults to providers.Singleton. bootstrap (bool, optional): Whether the provider should be bootstrapped. Defaults to False. diff --git a/stubs/dependency/core/declaration/product.pyi b/stubs/dependency/core/declaration/product.pyi index 54713d6..e9f3256 100644 --- a/stubs/dependency/core/declaration/product.pyi +++ b/stubs/dependency/core/declaration/product.pyi @@ -8,7 +8,7 @@ class Product(Component): """Product Base Class """ -def product(module: type[Module] | None = None, imports: Iterable[type[ProviderMixin]] = (), products: Iterable[type[ProviderMixin]] = (), provider: type[providers.Provider[Any]] = ..., partial_resolution: bool = False, bootstrap: bool = False) -> Callable[[type[COMPONENT]], type[COMPONENT]]: +def product(module: type[Module] | None = None, imports: Iterable[type[ProviderMixin]] = (), provider: type[providers.Provider[Any]] = ..., partial_resolution: bool = False, bootstrap: bool = False) -> Callable[[type[COMPONENT]], type[COMPONENT]]: """Decorator for Component class Args: diff --git a/stubs/dependency/core/injection/injectable.pyi b/stubs/dependency/core/injection/injectable.pyi index 1094d32..eb7b0fc 100644 --- a/stubs/dependency/core/injection/injectable.pyi +++ b/stubs/dependency/core/injection/injectable.pyi @@ -1,42 +1,21 @@ -from dependency.core.exceptions import CancelInitialization as CancelInitialization, DeclarationError as DeclarationError, InitializationError as InitializationError -from dependency_injector import containers as containers, providers as providers -from typing import Any, Callable, Iterable, Self +from typing import Any, Callable, Iterable class Injectable: """Injectable Class represents a implementation of some kind that can be injected as a dependency. Attributes: interface_cls (T): The interface class that this injectable implements. - - imports (Iterable[Injectable]): List of injectables that this injectable depends on. - products (Iterable[Injectable]): List of injectables that depend on this injectable. """ interface_cls: type modules_cls: set[type] implementation: type | None + bootstrap: Callable[[], Any] | None imports: set['Injectable'] - products: set['Injectable'] + dependent: set['Injectable'] partial_resolution: bool is_resolved: bool - def __init__(self, interface_cls: type) -> None: ... - @property - def import_resolved(self) -> bool: ... - def add_dependencies(self, imports: Iterable['Injectable'], products: Iterable['Injectable'], partial_resolution: bool = False) -> None: ... - def del_dependencies(self, imports: Iterable['Injectable'], products: Iterable['Injectable']) -> None: ... - def add_implementation(self, implementation: type, modules_cls: Iterable[type], provider: providers.Provider[Any], bootstrap: Callable[[], Any] | None = None) -> None: ... - @property - def provider(self) -> providers.Provider[Any]: - """Return the provider instance for this injectable.""" - @property - def provide(self) -> providers.Provider[Any]: - """Return the provide instance for this injectable.""" - def inject(self) -> Self: - """Mark the provider injection as resolved.""" - def wire(self, container: containers.DynamicContainer) -> None: - """Wire the provider with the given container. - - Args: - container (containers.DynamicContainer): Container to wire the provider with. - """ - def init(self) -> None: - """Execute the bootstrap function if it exists.""" + def __init__(self, interface_cls: type, implementation: type | None = None) -> None: ... + def check_resolved(self, providers: list['Injectable']) -> bool: ... + def update_dependencies(self, imports: Iterable['Injectable'], partial_resolution: bool | None = None) -> None: ... + def discard_dependencies(self, imports: Iterable['Injectable']) -> None: ... + def set_implementation(self, implementation: type, modules_cls: Iterable[type], bootstrap: Callable[[], Any] | None = None) -> None: ... diff --git a/stubs/dependency/core/injection/injection.pyi b/stubs/dependency/core/injection/injection.pyi index a6314e0..0636e86 100644 --- a/stubs/dependency/core/injection/injection.pyi +++ b/stubs/dependency/core/injection/injection.pyi @@ -1,8 +1,7 @@ import abc from abc import ABC, abstractmethod -from dependency.core.exceptions import ProvisionError as ProvisionError +from dependency.core.exceptions import DeclarationError as DeclarationError, ProvisionError as ProvisionError from dependency.core.injection.injectable import Injectable as Injectable -from dependency.core.injection.wiring import LazyProvide as LazyProvide from dependency_injector import containers, providers as providers from typing import Any, Generator, override @@ -22,15 +21,12 @@ class BaseInjection(ABC, metaclass=abc.ABCMeta): Args: parent (ContainerInjection): The new parent injection. """ - def validation(self) -> None: - """Validate the injection configuration.""" @abstractmethod def inject_cls(self) -> Any: """Return the class to be injected.""" @abstractmethod def resolve_providers(self) -> Generator[Injectable, None, None]: """Inject all children into the current injection context.""" - def __hash__(self) -> int: ... class ContainerInjection(BaseInjection): """Container Injection Class @@ -48,15 +44,23 @@ class ContainerInjection(BaseInjection): class ProviderInjection(BaseInjection): """Provider Injection Class """ - injectable: Injectable - def __init__(self, name: str, interface_cls: type, parent: ContainerInjection | None = None) -> None: ... + def __init__(self, name: str, injectable: Injectable, parent: ContainerInjection | None = None, provider: providers.Provider[Any] | None = None) -> None: ... + def set_provider(self, provider: providers.Provider[Any]) -> None: + """Set the provider instance for this injectable. + + Args: + provider (providers.Provider[Any]): The provider instance to set. + """ + @property + def injectable(self) -> Injectable: + """Return the injectable instance for this provider.""" @property @override def reference(self) -> str: """Return the reference for dependency injection.""" @property def provider(self) -> providers.Provider[Any]: - """Return the provider instance.""" + """Return the provider instance for this injectable.""" @override def inject_cls(self) -> providers.Provider[Any]: """Return the provider instance.""" diff --git a/stubs/dependency/core/injection/mixin.pyi b/stubs/dependency/core/injection/mixin.pyi index 732b75d..e3b996c 100644 --- a/stubs/dependency/core/injection/mixin.pyi +++ b/stubs/dependency/core/injection/mixin.pyi @@ -1,6 +1,8 @@ +from dependency.core.exceptions import DeclarationError as DeclarationError from dependency.core.injection.injectable import Injectable as Injectable from dependency.core.injection.injection import ContainerInjection as ContainerInjection, ProviderInjection as ProviderInjection from dependency.core.resolution.container import Container as Container +from dependency.core.resolution.registry import Registry as Registry from dependency_injector import providers as providers from typing import Any, Callable, Generator, Iterable @@ -16,6 +18,13 @@ class ContainerMixin: """Hook method called upon declaration of the container. """ @classmethod + def on_resolution(cls, container: Container) -> None: + """Hook method called upon resolution of the container. + + Args: + container (Container): The application container. + """ + @classmethod def init_injection(cls, parent: ContainerInjection | None) -> None: """Initialize the injection for the container. @@ -51,6 +60,7 @@ class ProviderMixin: injection (ProviderInjection): Injection handler for the provider """ injection: ProviderInjection + injectable: Injectable @classmethod def on_declaration(cls) -> None: """Hook method called upon declaration of the provider. @@ -75,16 +85,19 @@ class ProviderMixin: TypeError: If the class is not a subclass of the interface class. """ @classmethod - def set_dependencies(cls, imports: Iterable[type['ProviderMixin']] = (), products: Iterable[type['ProviderMixin']] = (), partial_resolution: bool = False) -> None: + def change_parent(cls, parent: type['ContainerMixin'] | None = None) -> None: + """Change the parent injection of this mixin. + """ + @classmethod + def update_dependencies(cls, imports: Iterable[type['ProviderMixin']] = (), partial_resolution: bool | None = None) -> None: '''Initialize the dependencies for the provider. Args: - imports (Iterable[type["ResolubleClass"]]): List of components to be imported by the provider. - products (Iterable[type["ResolubleClass"]]): List of products to be declared by the provider. + imports (Iterable[type["ResolubleClass"]]): List of providers to be imported by the provider. partial_resolution (bool, optional): Whether to allow partial resolution of dependencies. Defaults to False. ''' @classmethod - def remove_dependencies(cls, imports: Iterable[type['ProviderMixin']] = (), products: Iterable[type['ProviderMixin']] = ()) -> None: + def discard_dependencies(cls, imports: Iterable[type['ProviderMixin']] = ()) -> None: '''Remove dependencies from the provider. Args: @@ -92,10 +105,6 @@ class ProviderMixin: products (Iterable[type["ProviderMixin"]]): List of components to remove from products. ''' @classmethod - def change_parent(cls, parent: type['ContainerMixin'] | None = None) -> None: - """Change the parent injection of this mixin. - """ - @classmethod def reference(cls) -> str: """Return the reference name of the Injectable.""" @classmethod diff --git a/stubs/dependency/core/resolution/__init__.pyi b/stubs/dependency/core/resolution/__init__.pyi index 266ed4c..d686094 100644 --- a/stubs/dependency/core/resolution/__init__.pyi +++ b/stubs/dependency/core/resolution/__init__.pyi @@ -1,5 +1,6 @@ from dependency.core.resolution.container import Container as Container +from dependency.core.resolution.registry import Registry as Registry from dependency.core.resolution.resolver import InjectionResolver as InjectionResolver from dependency.core.resolution.strategy import ResolutionConfig as ResolutionConfig, ResolutionStrategy as ResolutionStrategy -__all__ = ['Container', 'InjectionResolver', 'ResolutionConfig', 'ResolutionStrategy'] +__all__ = ['Container', 'Registry', 'InjectionResolver', 'ResolutionConfig', 'ResolutionStrategy'] diff --git a/stubs/dependency/core/resolution/registry.pyi b/stubs/dependency/core/resolution/registry.pyi new file mode 100644 index 0000000..07aa018 --- /dev/null +++ b/stubs/dependency/core/resolution/registry.pyi @@ -0,0 +1,9 @@ +from dependency.core.injection.injectable import Injectable as Injectable +from dependency.core.injection.injection import ProviderInjection as ProviderInjection + +class Registry: + providers: set[ProviderInjection] + @classmethod + def register(cls, provider: ProviderInjection) -> None: ... + @classmethod + def validation(cls) -> None: ... diff --git a/stubs/dependency/core/resolution/resolver.pyi b/stubs/dependency/core/resolution/resolver.pyi index 29f93dc..06f3077 100644 --- a/stubs/dependency/core/resolution/resolver.pyi +++ b/stubs/dependency/core/resolution/resolver.pyi @@ -1,5 +1,7 @@ from dependency.core.injection.injectable import Injectable as Injectable +from dependency.core.injection.mixin import ContainerMixin as ContainerMixin from dependency.core.resolution.container import Container as Container +from dependency.core.resolution.registry import Registry as Registry from dependency.core.resolution.strategy import ResolutionStrategy as ResolutionStrategy from typing import Iterable @@ -7,13 +9,33 @@ class InjectionResolver: """Injection Resolver Class """ container: Container - providers: list[Injectable] - def __init__(self, container: Container, providers: Iterable[Injectable]) -> None: ... - def resolve_dependencies(self, strategy: type[ResolutionStrategy] = ...) -> list[Injectable]: + def __init__(self, container: Container) -> None: ... + def resolve_dependencies(self, modules: Iterable[type[ContainerMixin]], strategy: ResolutionStrategy = ...) -> list[Injectable]: + """Resolve dependencies for a list of modules. + + Args: + modules (Iterable[type[ContainerMixin]]): The list of module classes to resolve. + strategy (type[ResolutionStrategy]): The resolution strategy to use. + + Returns: + list[Injectable]: List of resolved injectables. + """ + def resolve_modules(self, modules: Iterable[type[ContainerMixin]]) -> list[Injectable]: + """Resolve all modules and their dependencies. + + Args: + modules (Iterable[type[ContainerMixin]]): The list of module classes to resolve. + + Returns: + list[Injectable]: List of resolved injectables from the modules. + """ + def resolve_providers(self, providers: Iterable[Injectable], strategy: ResolutionStrategy = ...) -> list[Injectable]: """Resolve all dependencies and initialize them. Args: + providers (Iterable[Injectable]): The list of providers to resolve. strategy (type[ResolutionStrategy]): The resolution strategy to use. Returns: - list[Injectable]: List of resolved injectables.""" + list[Injectable]: List of resolved injectables. + """ diff --git a/stubs/dependency/core/resolution/strategy.pyi b/stubs/dependency/core/resolution/strategy.pyi index b58f6dd..4a01ceb 100644 --- a/stubs/dependency/core/resolution/strategy.pyi +++ b/stubs/dependency/core/resolution/strategy.pyi @@ -1,3 +1,4 @@ +from dependency.core.exceptions import CancelInitialization as CancelInitialization, DeclarationError as DeclarationError, InitializationError as InitializationError from dependency.core.injection.injectable import Injectable as Injectable from dependency.core.resolution.container import Container as Container from dependency.core.resolution.errors import raise_resolution_error as raise_resolution_error @@ -12,40 +13,45 @@ class ResolutionStrategy: """Defines the strategy for resolving dependencies. """ config: ResolutionConfig - @classmethod - def resolution(cls, container: Container, providers: list[Injectable]) -> list[Injectable]: + def __init__(self, config: ResolutionConfig | None = None) -> None: ... + def resolution(self, providers: list[Injectable], container: Container) -> list[Injectable]: """Resolve all dependencies and initialize them. Args: + providers (list[Injectable]): List of providers to resolve. container (Container): The container to wire the injectables with. - providers (list[ProviderInjection]): List of provider injections to resolve. - config (ResolutionConfig): Configuration for the resolution strategy. Returns: list[Injectable]: List of resolved injectables. """ - @classmethod - def injection(cls, providers: list[Injectable]) -> list[Injectable]: + def expand(self, providers: list[Injectable]) -> list[Injectable]: + """Expand the list of providers by adding all their imports. + + Args: + providers (list[Injectable]): List of providers to expand. + + Returns: + list[Injectable]: List of expanded providers. + """ + def injection(self, providers: list[Injectable]) -> None: """Resolve all injectables in layers. Args: providers (list[Injectable]): List of injectables to resolve. Returns: - list[Injectable]: List of resolved injectables. + list[Injectable]: List of unresolved injectables. """ - @classmethod - def wiring(cls, container: Container, injectables: list[Injectable]) -> None: - """Wire a list of injectables with the given container. + def wiring(self, providers: list[Injectable], container: Container) -> None: + """Wire a list of providers with the given container. Args: - container (Container): The container to wire the injectables with. - injectables (list[Injectable]): List of injectables to wire. + providers (list[Injectable]): List of providers to wire. + container (Container): The container to wire the providers with. """ - @classmethod - def initialize(cls, injectables: list[Injectable]) -> None: + def initialize(self, providers: list[Injectable]) -> None: """Start all implementations by executing their init functions. Args: - injectables (list[Injectable]): List of injectables to start. + providers (list[Injectable]): List of providers to start. """ diff --git a/tests/core/test_declaration.py b/tests/core/test_declaration.py index 51380aa..ba033e5 100644 --- a/tests/core/test_declaration.py +++ b/tests/core/test_declaration.py @@ -2,6 +2,7 @@ from dependency_injector import providers from dependency.core.agrupation import Module, module from dependency.core.declaration import Component, component, instance +from dependency.core.injection import Injectable from dependency.core.resolution import Container @module() @@ -26,12 +27,12 @@ def method(self) -> str: def test_declaration() -> None: container = Container() TModule.inject_container(container) - for provider in TModule.injection.resolve_providers(): - provider.inject() + injectables: list[Injectable] = list(TModule.resolve_providers()) + for provider in injectables: + assert provider.check_resolved(injectables) - assert TComponent.injection.injectable.interface_cls == TComponent - assert TComponent.injection.injectable.implementation == TInstance - assert TInstance.injection.injectable.is_resolved + assert TComponent.injectable.interface_cls == TComponent + assert TComponent.injectable.implementation == TInstance component: TComponent = TComponent.provide() assert isinstance(component, TComponent) diff --git a/tests/core/test_exceptions.py b/tests/core/test_exceptions.py index 6d46c64..2cde373 100644 --- a/tests/core/test_exceptions.py +++ b/tests/core/test_exceptions.py @@ -1,4 +1,5 @@ import pytest +from dependency_injector import providers from dependency.core.agrupation import Plugin, PluginMeta, module from dependency.core.declaration import Component, component, instance from dependency.core.resolution import Container, ResolutionStrategy @@ -15,51 +16,61 @@ class TComponent1(Component): pass @component( - imports=[TComponent1], + imports=[ + TComponent1, + ], module=TPlugin, ) class TComponent2(Component): pass @component( - imports=[TComponent2], + imports=[ + TComponent2, + ], + provider=providers.Factory, partial_resolution=True, ) class TProduct1(Component): pass @instance( - imports=[TComponent1], - products=[TProduct1], + imports=[ + TComponent1, + TProduct1, + ], ) class TInstance1(TComponent1): pass def test_exceptions() -> None: + strategy: ResolutionStrategy = ResolutionStrategy() container = Container() - TPlugin.resolve_container(container) + TPlugin.resolve_container(container) with pytest.raises(DeclarationError): print(TComponent1.provide()) - with pytest.raises(DeclarationError): list(TPlugin.resolve_providers()) TComponent2.change_parent(None) injectables = list(TPlugin.resolve_providers()) - assert injectables == [TComponent1.injection.injectable] + assert set(injectables) == {TComponent1.injectable} + + injectables = strategy.expand(injectables) + assert set(injectables) == {TComponent1.injectable, TProduct1.injectable} with pytest.raises(ResolutionError): - ResolutionStrategy.injection(injectables) + strategy.injection(injectables) - TComponent1.remove_dependencies( + TComponent1.discard_dependencies( imports=[TComponent1], ) - ResolutionStrategy.injection(injectables) + strategy.injection(injectables) assert TComponent1.provide() - TProduct1.set_dependencies( + TProduct1.update_dependencies( partial_resolution=False, ) with pytest.raises(ResolutionError): - ResolutionStrategy.injection(injectables) + strategy.injection(injectables) diff --git a/tests/core/test_injection.py b/tests/core/test_injection.py index f7cb0dd..48fca87 100644 --- a/tests/core/test_injection.py +++ b/tests/core/test_injection.py @@ -17,14 +17,15 @@ def test(self, service: Instance = Provide[TEST_REFERENCE]) -> str: def test_injection1() -> None: container1 = ContainerInjection(name="container1") container2 = ContainerInjection(name="container2", parent=container1) - provider1 = ProviderInjection( - name="provider1", + + injectable1 = Injectable( interface_cls=Interface, - parent=container2 - ) - provider1.injectable.add_implementation( implementation=Instance, - modules_cls={Interface}, + ) + provider1 = ProviderInjection( + name="provider1", + injectable=injectable1, + parent=container2, provider=providers.Singleton(Instance), ) assert provider1.reference == TEST_REFERENCE @@ -37,7 +38,7 @@ def test_injection1() -> None: Interface().test() for provider in list(container1.resolve_providers()): - provider.wire(container=container) + container.wire(provider.modules_cls) container.wire((Interface,)) assert Interface().test() == "Injected service: Test method called" diff --git a/tests/core/test_interfaces.py b/tests/core/test_interfaces.py index be1b4e1..ba14dce 100644 --- a/tests/core/test_interfaces.py +++ b/tests/core/test_interfaces.py @@ -3,7 +3,7 @@ from dependency_injector.wiring import inject from dependency.core.agrupation import Module, module from dependency.core.declaration import Component, component -from dependency.core.injection import LazyProvide +from dependency.core.injection import Injectable, LazyProvide from dependency.core.resolution import Container @module() @@ -48,17 +48,18 @@ def test_interfaces() -> None: container = Container() TModule.inject_container(container) - injectables = list(TModule.resolve_providers()) + injectables: list[Injectable] = list(TModule.resolve_providers()) for injectable in injectables: - injectable.inject() - for injectable in injectables: - injectable.wire(container) + injectable.check_resolved(injectables) + assert TProduct1.injectable.check_resolved(injectables) + assert TProduct2.injectable.check_resolved(injectables) + for injectable in injectables: + container.wire(injectable.modules_cls) product1: TProduct1 = TProduct1.provide() product2: TProduct2 = TProduct2.provide() assert isinstance(product1, TProduct1) assert isinstance(product2, TProduct2) - assert product1.method() == "Hello, World!" assert product2.method() == "Hello, World!" diff --git a/tests/core/test_products.py b/tests/core/test_products.py index d239802..a6e57d7 100644 --- a/tests/core/test_products.py +++ b/tests/core/test_products.py @@ -15,14 +15,12 @@ class TPlugin(Plugin): class TComponent1(Component): pass -@component( -) +@component() class TComponent2(Component): pass @component( imports=[ - TComponent1, TComponent2, ], provider=providers.Factory, @@ -31,23 +29,28 @@ class TProduct1(Component): pass @instance( - products=[ - TProduct1 + imports=[ + TProduct1, ], ) class TInstance1(TComponent1): pass def test_products() -> None: + strategy: ResolutionStrategy = ResolutionStrategy() container = Container() + TPlugin.resolve_container(container) injectables = list(TPlugin.resolve_providers()) + assert injectables == [TComponent1.injectable] with pytest.raises(ResolutionError): - ResolutionStrategy.injection(injectables) + expanded = strategy.expand(injectables) + strategy.injection(expanded) - TProduct1.injection.injectable.partial_resolution = True - injectables = ResolutionStrategy.injection(injectables) + TProduct1.injectable.partial_resolution = True + expanded = strategy.expand(injectables) + strategy.injection(expanded) - assert TComponent1.injection.injectable in injectables - assert TProduct1.injection.injectable in injectables + assert TComponent1.injectable.is_resolved + assert TProduct1.injectable.is_resolved diff --git a/tests/core/test_providers.py b/tests/core/test_providers.py index ab1ebca..101158c 100644 --- a/tests/core/test_providers.py +++ b/tests/core/test_providers.py @@ -2,6 +2,7 @@ from dependency_injector import containers, providers from dependency.core.agrupation import Module, module from dependency.core.declaration import Component, component +from dependency.core.injection import Injectable @module() class TModule(Module): @@ -42,8 +43,9 @@ class TComponent(TProduct): def test_providers() -> None: container = containers.DynamicContainer() setattr(container, TModule.injection.name, TModule.injection.inject_cls()) - for provider in TModule.injection.resolve_providers(): - provider.inject() + providers: list[Injectable] = list(TModule.resolve_providers()) + for provider in providers: + assert provider.check_resolved(providers) product1: TProduct1 = TComponent.provide("product1") product2: TProduct2 = TComponent.provide("product2") diff --git a/tests/core/test_resolution.py b/tests/core/test_resolution.py index e6efda0..de4faa1 100644 --- a/tests/core/test_resolution.py +++ b/tests/core/test_resolution.py @@ -1,4 +1,5 @@ import pytest +from dependency_injector import providers from dependency.core.agrupation import Plugin, PluginMeta, module from dependency.core.declaration import Component, component, instance, providers from dependency.core.resolution import Container, InjectionResolver @@ -23,14 +24,15 @@ class TComponent2(Component): pass @component( - imports=[TComponent1], provider=providers.Factory, ) class TProduct1(Component): pass @instance( - products=[TProduct1], + imports=[ + TProduct1, + ], bootstrap=True, ) class TInstance1(TComponent1): @@ -46,15 +48,15 @@ def __init__(self) -> None: BOOTSTRAPED.append("TInstance2") raise CancelInitialization("Failed to initialize TInstance2") -def test_exceptions() -> None: +def test_resolution() -> None: container = Container.from_json("example/config.json") injectables = TPlugin.resolve_providers() assert "TInstance1" not in BOOTSTRAPED - loader = InjectionResolver(container, injectables) + loader = InjectionResolver(container) assert "TInstance1" not in BOOTSTRAPED - loader.resolve_dependencies() + loader.resolve_providers(injectables) assert "TInstance1" in BOOTSTRAPED assert "TInstance2" in BOOTSTRAPED diff --git a/tests/core/test_resource.py b/tests/core/test_resource.py index 13a493f..450887d 100644 --- a/tests/core/test_resource.py +++ b/tests/core/test_resource.py @@ -24,13 +24,14 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore self.initialized = False def test_resource() -> None: + strategy: ResolutionStrategy = ResolutionStrategy() container = Container() + TPlugin.resolve_container(container) injectables = list(TPlugin.resolve_providers()) - assert TInstance.initialized == False - ResolutionStrategy.resolution(container, injectables) + strategy.resolution(injectables, container) component: TComponent = TComponent.provide() assert component.initialized == True @@ -38,4 +39,4 @@ def test_resource() -> None: #container.shutdown_resources() TComponent.provider().shutdown() # type: ignore assert component.initialized == False - assert injectables == [TComponent.injection.injectable] + assert injectables == [TComponent.injectable] diff --git a/tests/core/test_validation.py b/tests/core/test_validation.py index 7c6d006..6551da6 100644 --- a/tests/core/test_validation.py +++ b/tests/core/test_validation.py @@ -28,7 +28,9 @@ class TInstance2(TComponent1): pass def test_validation() -> None: + strategy: ResolutionStrategy = ResolutionStrategy() container = Container() + with pytest.raises(ProvisionError): TPlugin.resolve_container(container) @@ -36,9 +38,9 @@ def test_validation() -> None: TPlugin.resolve_container(container) injectables = list(TPlugin.resolve_providers()) - ResolutionStrategy.injection(injectables) - assert TComponent1.injection.injectable.implementation != TInstance1 - assert TComponent1.injection.injectable.implementation == TInstance2 + strategy.injection(injectables) + assert TComponent1.injectable.implementation != TInstance1 + assert TComponent1.injectable.implementation == TInstance2 assert TComponent1.provider() == TInstance2.provider() assert TComponent1.provide() == TInstance2.provide() diff --git a/tests/example/test_module.py b/tests/example/test_module.py index ca81fab..f934c71 100644 --- a/tests/example/test_module.py +++ b/tests/example/test_module.py @@ -8,7 +8,7 @@ class TestingModule(Module): pass -def test_change_parent_and_resolve(): +def test_module(): for component in ( NumberService, StringService, @@ -25,12 +25,12 @@ def test_change_parent_and_resolve(): TestingModule.inject_container(container) loader = InjectionResolver( container=container, + ) + injectables = loader.resolve_providers( providers=TestingModule.resolve_providers(), ) - injectables = loader.resolve_dependencies() - - assert HardwareFactory.injection.injectable in injectables - assert HardwareFactory.injection.injectable.is_resolved + assert HardwareFactory.injectable in injectables + assert HardwareFactory.injectable.is_resolved number_service: NumberService = NumberService.provide(starting_number=40) assert number_service.getRandomNumber() == 40