From 287f5e31496fb329d309e9febe8a9d66cda44638 Mon Sep 17 00:00:00 2001 From: edraft Date: Wed, 24 Sep 2025 21:27:28 +0200 Subject: [PATCH] New implementation of scopes #186 --- example/custom/di/src/di/application.py | 19 ++++++++------- example/custom/di/src/di/di_tester_service.py | 4 ++++ example/custom/di/src/di/startup.py | 6 +++-- example/custom/di/src/di/test1_service.py | 2 +- example/custom/di/src/di/test2_service.py | 2 +- example/custom/di/src/di/test_service.py | 6 +++-- .../custom/general/src/general/application.py | 24 ++++++++++++++----- .../general/src/general/scoped_service.py | 10 ++++++++ example/custom/general/src/general/startup.py | 2 ++ .../cpl/api/middleware/_scope_middleware.py | 13 ++++++++++ src/cpl-core/cpl/core/utils/string.py | 11 +++++---- .../cpl/dependency/service_provider.py | 18 +++++++++++++- 12 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 example/custom/general/src/general/scoped_service.py create mode 100644 src/cpl-api/cpl/api/middleware/_scope_middleware.py diff --git a/example/custom/di/src/di/application.py b/example/custom/di/src/di/application.py index e2501429..b8a62dd6 100644 --- a/example/custom/di/src/di/application.py +++ b/example/custom/di/src/di/application.py @@ -1,7 +1,6 @@ from cpl.application.abc import ApplicationABC from cpl.core.console.console import Console from cpl.dependency import ServiceProvider -from cpl.dependency.scope import Scope from di.static_test import StaticTest from di.test_abc import TestABC from di.test_service import TestService @@ -17,26 +16,30 @@ class Application(ApplicationABC): ts: TestService = self._services.get_service(TestService) ts.run() - def configure(self): ... - def main(self): with self._services.create_scope() as scope: Console.write_line("Scope1") - ts: TestService = scope.service_provider.get_service(TestService) + ts: TestService = scope.get_service(TestService) ts.run() - dit: DITesterService = scope.service_provider.get_service(DITesterService) + dit: DITesterService = scope.get_service(DITesterService) dit.run() + if ts.name != dit.name: + raise Exception("DI is broken!") + with self._services.create_scope() as scope: Console.write_line("Scope2") - ts: TestService = scope.service_provider.get_service(TestService) + ts: TestService = scope.get_service(TestService) ts.run() - dit: DITesterService = scope.service_provider.get_service(DITesterService) + dit: DITesterService = scope.get_service(DITesterService) dit.run() + if ts.name != dit.name: + raise Exception("DI is broken!") + Console.write_line("Global") self._part_of_scoped() StaticTest.test() self._services.get_service(Tester) - Console.write_line(self._services.get_services(list[TestABC])) + Console.write_line(self._services.get_services(TestABC)) diff --git a/example/custom/di/src/di/di_tester_service.py b/example/custom/di/src/di/di_tester_service.py index d598f44d..9937f561 100644 --- a/example/custom/di/src/di/di_tester_service.py +++ b/example/custom/di/src/di/di_tester_service.py @@ -6,6 +6,10 @@ class DITesterService: def __init__(self, ts: TestService): self._ts = ts + @property + def name(self) -> str: + return self._ts.name + def run(self): Console.write_line("DIT: ") self._ts.run() diff --git a/example/custom/di/src/di/startup.py b/example/custom/di/src/di/startup.py index 2a8a430b..89e7e1a7 100644 --- a/example/custom/di/src/di/startup.py +++ b/example/custom/di/src/di/startup.py @@ -12,9 +12,11 @@ class Startup(StartupABC): def __init__(self): StartupABC.__init__(self) - def configure_configuration(self): ... + @staticmethod + def configure_configuration(): ... - def configure_services(self, services: ServiceCollection) -> ServiceProvider: + @staticmethod + def configure_services(services: ServiceCollection) -> ServiceProvider: services.add_scoped(TestService) services.add_scoped(DITesterService) diff --git a/example/custom/di/src/di/test1_service.py b/example/custom/di/src/di/test1_service.py index 2080696f..21852f49 100644 --- a/example/custom/di/src/di/test1_service.py +++ b/example/custom/di/src/di/test1_service.py @@ -6,7 +6,7 @@ from di.test_abc import TestABC class Test1Service(TestABC): def __init__(self): - TestABC.__init__(self, String.random_string(string.ascii_lowercase, 8)) + TestABC.__init__(self, String.random(8)) def run(self): Console.write_line(f"Im {self._name}") diff --git a/example/custom/di/src/di/test2_service.py b/example/custom/di/src/di/test2_service.py index 62c756c2..06832778 100644 --- a/example/custom/di/src/di/test2_service.py +++ b/example/custom/di/src/di/test2_service.py @@ -6,7 +6,7 @@ from di.test_abc import TestABC class Test2Service(TestABC): def __init__(self): - TestABC.__init__(self, String.random_string(string.ascii_lowercase, 8)) + TestABC.__init__(self, String.random(8)) def run(self): Console.write_line(f"Im {self._name}") diff --git a/example/custom/di/src/di/test_service.py b/example/custom/di/src/di/test_service.py index 8f89c94c..893cb29b 100644 --- a/example/custom/di/src/di/test_service.py +++ b/example/custom/di/src/di/test_service.py @@ -1,5 +1,3 @@ -import string - from cpl.core.console.console import Console from cpl.core.utils.string import String @@ -8,5 +6,9 @@ class TestService: def __init__(self): self._name = String.random(8) + @property + def name(self) -> str: + return self._name + def run(self): Console.write_line(f"Im {self._name}") diff --git a/example/custom/general/src/general/application.py b/example/custom/general/src/general/application.py index 8bae265b..b22ce377 100644 --- a/example/custom/general/src/general/application.py +++ b/example/custom/general/src/general/application.py @@ -9,7 +9,8 @@ from cpl.core.environment import Environment from cpl.core.log import LoggerABC from cpl.core.pipes import IPAddressPipe from cpl.mail import EMail, EMailClientABC -from cpl.query.extension.list import List +from cpl.query import List +from general.scoped_service import ScopedService from test_service import TestService from test_settings import TestSettings @@ -38,7 +39,7 @@ class Application(ApplicationABC): def main(self): self._logger.debug(f"Host: {Environment.get_host_name()}") self._logger.debug(f"Environment: {Environment.get_environment()}") - Console.write_line(List(int, range(0, 10)).select(lambda x: f"x={x}").to_list()) + Console.write_line(List(range(0, 10)).select(lambda x: f"x={x}").to_list()) Console.spinner("Test", self._wait, 2, spinner_foreground_color="red") test: TestService = self._services.get_service(TestService) ip_pipe: IPAddressPipe = self._services.get_service(IPAddressPipe) @@ -48,10 +49,21 @@ class Application(ApplicationABC): Console.write_line(f"DI working: {test == test2 and ip_pipe != ip_pipe2}") Console.write_line(self._services.get_service(LoggerABC)) - scope = self._services.create_scope() - Console.write_line("scope", scope) - with self._services.create_scope() as s: - Console.write_line("with scope", s) + root_scoped_service = self._services.get_service(ScopedService) + with self._services.create_scope() as scope: + s_srvc1 = scope.get_service(ScopedService) + s_srvc2 = scope.get_service(ScopedService) + + Console.write_line(root_scoped_service) + Console.write_line(s_srvc1) + Console.write_line(s_srvc2) + if root_scoped_service == s_srvc1 or s_srvc1 != s_srvc2: + raise Exception("Root scoped service should not be equal to scoped service") + + root_scoped_service2 = self._services.get_service(ScopedService) + Console.write_line(root_scoped_service2) + if root_scoped_service != root_scoped_service2: + raise Exception("Root scoped service should be equal to root scoped service 2") test_settings = Configuration.get(TestSettings) Console.write_line(test_settings.value) diff --git a/example/custom/general/src/general/scoped_service.py b/example/custom/general/src/general/scoped_service.py new file mode 100644 index 00000000..93e15142 --- /dev/null +++ b/example/custom/general/src/general/scoped_service.py @@ -0,0 +1,10 @@ +from cpl.core.console import Console + + +class ScopedService: + def __init__(self): + self.value = "I am a scoped service" + Console.write_line(self.value, self) + + def get_value(self): + return self.value diff --git a/example/custom/general/src/general/startup.py b/example/custom/general/src/general/startup.py index 5ba00057..27e3ceba 100644 --- a/example/custom/general/src/general/startup.py +++ b/example/custom/general/src/general/startup.py @@ -4,6 +4,7 @@ from cpl.core.configuration import Configuration from cpl.core.environment import Environment from cpl.core.pipes import IPAddressPipe from cpl.dependency import ServiceCollection +from general.scoped_service import ScopedService from test_service import TestService @@ -21,3 +22,4 @@ class Startup(StartupABC): services.add_module(mail) services.add_transient(IPAddressPipe) services.add_singleton(TestService) + services.add_scoped(ScopedService) diff --git a/src/cpl-api/cpl/api/middleware/_scope_middleware.py b/src/cpl-api/cpl/api/middleware/_scope_middleware.py new file mode 100644 index 00000000..cc49631f --- /dev/null +++ b/src/cpl-api/cpl/api/middleware/_scope_middleware.py @@ -0,0 +1,13 @@ +from cpl.api.abc import ASGIMiddleware +from cpl.dependency.service_provider import ServiceProvider + + +class ScopeMiddleware(ASGIMiddleware): + def __init__(self, app, provider: ServiceProvider): + ASGIMiddleware.__init__(self, app) + self._app = app + self._provider = provider + + async def __call__(self, scope, receive, send): + with self._provider.create_scope(): + await self._app(scope, receive, send) \ No newline at end of file diff --git a/src/cpl-core/cpl/core/utils/string.py b/src/cpl-core/cpl/core/utils/string.py index fb11026c..672a3168 100644 --- a/src/cpl-core/cpl/core/utils/string.py +++ b/src/cpl-core/cpl/core/utils/string.py @@ -114,12 +114,15 @@ class String: characters = [] if letters: - characters.append(string.ascii_letters) + characters.extend(string.ascii_letters) if digits: - characters.append(string.digits) + characters.extend(string.digits) if special_characters: - characters.append(string.punctuation) + characters.extend(string.punctuation) - return "".join(random.choice(characters) for _ in range(length)) if characters else "" + x = "".join(random.choice(list(characters)) for _ in range(length)) if characters else "" + if len(x) != length: + raise Exception("No characters selected to generate random string") + return x diff --git a/src/cpl-dependency/cpl/dependency/service_provider.py b/src/cpl-dependency/cpl/dependency/service_provider.py index b82e658c..09fc3852 100644 --- a/src/cpl-dependency/cpl/dependency/service_provider.py +++ b/src/cpl-dependency/cpl/dependency/service_provider.py @@ -1,4 +1,6 @@ +import copy import typing +from contextlib import contextmanager from inspect import signature, Parameter, Signature from typing import Optional, Type @@ -6,6 +8,7 @@ from cpl.core.configuration import Configuration from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC from cpl.core.environment import Environment from cpl.core.typing import T, R, Source +from cpl.dependency import use_provider from cpl.dependency.service_descriptor import ServiceDescriptor from cpl.dependency.service_lifetime import ServiceLifetimeEnum @@ -132,6 +135,19 @@ class ServiceProvider: return service_type(*params, *args, **kwargs) + @contextmanager + def create_scope(self): + scoped_descriptors = [] + for d in self._service_descriptors: + if d.lifetime == ServiceLifetimeEnum.singleton: + scoped_descriptors.append(d) + else: + scoped_descriptors.append(copy.deepcopy(d)) + + scoped_provider = ServiceProvider(scoped_descriptors) + with use_provider(scoped_provider): + yield scoped_provider + def get_service(self, service_type: T, *args, **kwargs) -> Optional[R]: result = self._find_service(service_type) @@ -143,7 +159,7 @@ class ServiceProvider: implementation = self._build_service(service_type, *args, **kwargs) if ( - result.lifetime == ServiceLifetimeEnum.singleton + result.lifetime in (ServiceLifetimeEnum.singleton, ServiceLifetimeEnum.scoped) ): result.implementation = implementation