diff --git a/src/cpl_core/dependency_injection/scope.py b/src/cpl_core/dependency_injection/scope.py new file mode 100644 index 00000000..a461fcdd --- /dev/null +++ b/src/cpl_core/dependency_injection/scope.py @@ -0,0 +1,18 @@ + +from cpl_core.console.console import Console +from cpl_core.dependency_injection.scope_abc import ScopeABC +from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC + + +class Scope(ScopeABC): + + def __init__(self, service_provider: ServiceProviderABC): + self._service_provider = service_provider + self._service_provider.set_scope(self) + + @property + def service_provider(self) -> ServiceProviderABC: + return self._service_provider + + def dispose(self): + self._service_provider = None diff --git a/src/cpl_core/dependency_injection/scope_abc.py b/src/cpl_core/dependency_injection/scope_abc.py new file mode 100644 index 00000000..4de16c74 --- /dev/null +++ b/src/cpl_core/dependency_injection/scope_abc.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod + +class ScopeABC(ABC): + r"""ABC for the class :class:`cpl_core.dependency_injection.scope.Scope`""" + + def __init__(self): + pass + + @property + @abstractmethod + def service_provider(self): + r"""Returns to service provider of scope + + Returns + ------- + Object of type :class:`cpl_core.dependency_injection.service_provider_abc.ServiceProviderABC` + """ + pass + + @abstractmethod + def dispose(): + r"""Sets service_provider to None + """ + pass \ No newline at end of file diff --git a/src/cpl_core/dependency_injection/scope_builder.py b/src/cpl_core/dependency_injection/scope_builder.py new file mode 100644 index 00000000..595b9a80 --- /dev/null +++ b/src/cpl_core/dependency_injection/scope_builder.py @@ -0,0 +1,19 @@ +from cpl_core.dependency_injection.scope import Scope +from cpl_core.dependency_injection.scope_abc import ScopeABC +from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC + + +class ScopeBuilder: + r"""Class to build :class:`cpl_core.dependency_injection.scope.Scope`""" + + def __init__(self, service_provider: ServiceProviderABC) -> None: + self._service_provider = service_provider + + def build(self) -> ScopeABC: + r"""Returns scope + + Returns + ------- + Object of type :class:`cpl_core.dependency_injection.scope.Scope` + """ + return Scope(self._service_provider) diff --git a/src/cpl_core/dependency_injection/service_lifetime_enum.py b/src/cpl_core/dependency_injection/service_lifetime_enum.py index c2057ba8..0b4cf16d 100644 --- a/src/cpl_core/dependency_injection/service_lifetime_enum.py +++ b/src/cpl_core/dependency_injection/service_lifetime_enum.py @@ -4,5 +4,5 @@ from enum import Enum class ServiceLifetimeEnum(Enum): singleton = 0 - scoped = 1 # not supported yet + scoped = 1 transient = 2 diff --git a/src/cpl_core/dependency_injection/service_provider.py b/src/cpl_core/dependency_injection/service_provider.py index cf079d83..04a19b57 100644 --- a/src/cpl_core/dependency_injection/service_provider.py +++ b/src/cpl_core/dependency_injection/service_provider.py @@ -1,10 +1,13 @@ from collections import Callable +import copy from inspect import signature, Parameter from typing import Optional from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC from cpl_core.database.context.database_context_abc import DatabaseContextABC +from cpl_core.dependency_injection.scope_abc import ScopeABC +from cpl_core.dependency_injection.scope_builder import ScopeBuilder from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC from cpl_core.dependency_injection.service_descriptor import ServiceDescriptor from cpl_core.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum @@ -30,6 +33,7 @@ class ServiceProvider(ServiceProviderABC): self._service_descriptors: list[ServiceDescriptor] = service_descriptors self._configuration: ConfigurationABC = config self._database_context = db_context + self._scope: Optional[ScopeABC] = None def _find_service(self, service_type: type) -> ServiceDescriptor: for descriptor in self._service_descriptors: @@ -84,8 +88,15 @@ class ServiceProvider(ServiceProviderABC): params.append(self._get_service(parameter)) return service_type(*params) + + def set_scope(self, scope: ScopeABC): + self._scope = scope + + def create_scope(self) -> ScopeABC: + sb = ScopeBuilder(ServiceProvider(copy.deepcopy(self._service_descriptors), self._configuration, self._database_context)) + return sb.build() - def get_service(self, service_type: type) -> Optional[Callable]: + def get_service(self, service_type: type) -> Optional[Callable[object]]: result = self._find_service(service_type) if result is None: @@ -95,7 +106,7 @@ class ServiceProvider(ServiceProviderABC): return result.implementation implementation = self.build_service(service_type) - if result.lifetime == ServiceLifetimeEnum.singleton: + if result.lifetime == ServiceLifetimeEnum.singleton or result.lifetime == ServiceLifetimeEnum.scoped and self._scope is not None: result.implementation = implementation return implementation diff --git a/src/cpl_core/dependency_injection/service_provider_abc.py b/src/cpl_core/dependency_injection/service_provider_abc.py index 6c64a4e3..cc06e529 100644 --- a/src/cpl_core/dependency_injection/service_provider_abc.py +++ b/src/cpl_core/dependency_injection/service_provider_abc.py @@ -2,6 +2,8 @@ from abc import abstractmethod, ABC from collections import Callable from typing import Type, Optional +from cpl_core.dependency_injection.scope_abc import ScopeABC + class ServiceProviderABC(ABC): r"""ABC for the class :class:`cpl_core.dependency_injection.service_provider.ServiceProvider`""" @@ -24,9 +26,30 @@ class ServiceProviderABC(ABC): Object of the given type """ pass + + @abstractmethod + def set_scope(self, scope: ScopeABC): + r"""Sets the scope of service provider + + Parameter + --------- + scope :class:`cpl_core.dependency_injection.scope.Scope` + Service scope + """ + pass + + @abstractmethod + def create_scope(self) -> ScopeABC: + r"""Creates a service scope + + Returns + ------- + Object of type :class:`cpl_core.dependency_injection.scope.Scope` + """ + pass @abstractmethod - def get_service(self, instance_type: Type) -> Optional[Callable]: + def get_service(self, instance_type: Type) -> Optional[Callable[object]]: r"""Returns instance of given type Parameter diff --git a/src/tests/custom/di/LICENSE b/src/tests/custom/di/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/custom/di/README.md b/src/tests/custom/di/README.md new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/custom/di/appsettings.json b/src/tests/custom/di/appsettings.json new file mode 100644 index 00000000..629e6ebd --- /dev/null +++ b/src/tests/custom/di/appsettings.json @@ -0,0 +1,15 @@ +{ + "TimeFormatSettings": { + "DateFormat": "%Y-%m-%d", + "TimeFormat": "%H:%M:%S", + "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", + "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" + }, + + "LoggingSettings": { + "Path": "logs/", + "Filename": "log_$start_time.log", + "ConsoleLogLevel": "ERROR", + "FileLogLevel": "WARN" + } +} diff --git a/src/tests/custom/di/cpl-workspace.json b/src/tests/custom/di/cpl-workspace.json new file mode 100644 index 00000000..4e95d495 --- /dev/null +++ b/src/tests/custom/di/cpl-workspace.json @@ -0,0 +1,9 @@ +{ + "WorkspaceSettings": { + "DefaultProject": "di", + "Projects": { + "di": "src/di/di.json" + }, + "Scripts": {} + } +} \ No newline at end of file diff --git a/src/tests/custom/di/src/di/__init__.py b/src/tests/custom/di/src/di/__init__.py new file mode 100644 index 00000000..ad5eca30 --- /dev/null +++ b/src/tests/custom/di/src/di/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/src/tests/custom/di/src/di/application.py b/src/tests/custom/di/src/di/application.py new file mode 100644 index 00000000..e9f0b4bf --- /dev/null +++ b/src/tests/custom/di/src/di/application.py @@ -0,0 +1,45 @@ +from cpl_core.application import ApplicationABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.console.console import Console +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_core.dependency_injection.scope import Scope +from test_service_service import TestService +from di_tester_service import DITesterService + + +class Application(ApplicationABC): + + def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): + ApplicationABC.__init__(self, config, services) + + def _part_of_scoped(self): + ts: TestService = self._services.get_service(TestService) + ts.run() + + def configure(self): + pass + + def main(self): + Console.write_line('Scope1') + scope1: Scope = self._services.create_scope() + ts: TestService = scope1.service_provider.get_service(TestService) + ts.run() + dit: DITesterService = scope1.service_provider.get_service(DITesterService) + dit.run() + t = scope1 + b = t.service_provider + scope1.dispose() + + Console.write_line('Disposed:') + ts1: TestService = scope1.service_provider.get_service(TestService) + ts1.run() + + Console.write_line('Scope2') + scope2: Scope = self._services.create_scope() + ts: TestService = scope2.service_provider.get_service(TestService) + ts.run() + dit: DITesterService = scope2.service_provider.get_service(DITesterService) + dit.run() + + Console.write_line('Global') + self._part_of_scoped() diff --git a/src/tests/custom/di/src/di/di.json b/src/tests/custom/di/src/di/di.json new file mode 100644 index 00000000..cd4e3ff2 --- /dev/null +++ b/src/tests/custom/di/src/di/di.json @@ -0,0 +1,43 @@ +{ + "ProjectSettings": { + "Name": "di", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "sh_cpl>=2021.10.0.post1" + ], + "PythonVersion": ">=3.9.2", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "console", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "di.main", + "EntryPoint": "di", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/tests/custom/di/src/di/di_tester_service.py b/src/tests/custom/di/src/di/di_tester_service.py new file mode 100644 index 00000000..27a40b85 --- /dev/null +++ b/src/tests/custom/di/src/di/di_tester_service.py @@ -0,0 +1,12 @@ +from cpl_core.console.console import Console +from test_service_service import TestService + + +class DITesterService: + + def __init__(self, ts: TestService): + self._ts = ts + + def run(self): + Console.write_line('DIT: ') + self._ts.run() diff --git a/src/tests/custom/di/src/di/main.py b/src/tests/custom/di/src/di/main.py new file mode 100644 index 00000000..4289aee6 --- /dev/null +++ b/src/tests/custom/di/src/di/main.py @@ -0,0 +1,14 @@ +from cpl_core.application import ApplicationBuilder + +from di.application import Application +from di.startup import Startup + + +def main(): + app_builder = ApplicationBuilder(Application) + app_builder.use_startup(Startup) + app_builder.build().run() + + +if __name__ == '__main__': + main() diff --git a/src/tests/custom/di/src/di/startup.py b/src/tests/custom/di/src/di/startup.py new file mode 100644 index 00000000..9f56c8be --- /dev/null +++ b/src/tests/custom/di/src/di/startup.py @@ -0,0 +1,21 @@ +from cpl_core.application import StartupABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceProviderABC, ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironment +from test_service_service import TestService +from di_tester_service import DITesterService + + +class Startup(StartupABC): + + def __init__(self): + StartupABC.__init__(self) + + def configure_configuration(self, configuration: ConfigurationABC, environment: ApplicationEnvironment) -> ConfigurationABC: + return configuration + + def configure_services(self, services: ServiceCollectionABC, environment: ApplicationEnvironment) -> ServiceProviderABC: + services.add_scoped(TestService) + services.add_scoped(DITesterService) + + return services.build_service_provider() diff --git a/src/tests/custom/di/src/di/test_service_service.py b/src/tests/custom/di/src/di/test_service_service.py new file mode 100644 index 00000000..f99a47a1 --- /dev/null +++ b/src/tests/custom/di/src/di/test_service_service.py @@ -0,0 +1,13 @@ +import string +from cpl_core.console.console import Console +from cpl_core.utils.string import String + + +class TestService: + + def __init__(self): + self._name = String.random_string(string.ascii_lowercase, 8) + + + def run(self): + Console.write_line(f'Im {self._name}') \ No newline at end of file diff --git a/src/tests/custom/di/src/tests/__init__.py b/src/tests/custom/di/src/tests/__init__.py new file mode 100644 index 00000000..ad5eca30 --- /dev/null +++ b/src/tests/custom/di/src/tests/__init__.py @@ -0,0 +1 @@ +# imports: