From e4183ff5aa3528e6afc14f80ff9cac297d32e437 Mon Sep 17 00:00:00 2001 From: Elon Portugaly Date: Thu, 5 Sep 2024 22:52:43 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(rats.apps):=20New=20decorators?= =?UTF-8?q?=20factory=5Fservice=20and=20autoid=5Ffactory=5Fservice=20simpl?= =?UTF-8?q?ify=20creating=20services=20that=20are=20factories.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rats-apps/src/python/rats/apps/__init__.py | 4 ++ .../src/python/rats/apps/_annotations.py | 53 ++++++++++++++++++- .../apps/example/_dummy_containers.py | 13 +++++ .../rats_test/apps/test_service_caching.py | 28 ++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/rats-apps/src/python/rats/apps/__init__.py b/rats-apps/src/python/rats/apps/__init__.py index 063d385c..b93d533a 100644 --- a/rats-apps/src/python/rats/apps/__init__.py +++ b/rats-apps/src/python/rats/apps/__init__.py @@ -12,6 +12,8 @@ fallback_service, group, service, + factory_service, + autoid_factory_service, ) from ._composite_container import CompositeContainer from ._container import ( @@ -63,4 +65,6 @@ "StandardRuntime", "StandardRuntime", "SimpleApplication", + "factory_service", + "autoid_factory_service", ] diff --git a/rats-apps/src/python/rats/apps/_annotations.py b/rats-apps/src/python/rats/apps/_annotations.py index bf21fde6..c19fdd96 100644 --- a/rats-apps/src/python/rats/apps/_annotations.py +++ b/rats-apps/src/python/rats/apps/_annotations.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Any, NamedTuple, ParamSpec, cast +from typing import Any, NamedTuple, ParamSpec, cast, Concatenate, TypeVar, Generic from rats import annotations @@ -8,6 +8,8 @@ from ._scoping import scope_service_name P = ParamSpec("P") +R = TypeVar("R") +T_Container = TypeVar("T_Container") def service( @@ -49,6 +51,55 @@ def fallback_group( ) +def _factory_to_factory_provider( + method: Callable[Concatenate[T_Container, P], R], +) -> Callable[[T_Container], Callable[P, R]]: + """Convert a factory method a factory provider method returning the original method.""" + + def new_method(self: T_Container) -> Callable[P, R]: + def factory(*args: P.args, **kwargs: P.kwargs) -> R: + return method(self, *args, **kwargs) + + return factory + + new_method.__name__ = method.__name__ + new_method.__module__ = method.__module__ + new_method.__qualname__ = method.__qualname__ + new_method.__doc__ = method.__doc__ + return new_method + + +class factory_service(Generic[P, R]): + """A decorator to create a factory service. + + Decorate a method that takes any number of arguments and returns an object. The resulting + service will be that factory - taking the same arguments and returning a new object each time. + """ + + _service_id: ServiceId[Callable[P, R]] + + def __init__(self, service_id: ServiceId[Callable[P, R]]) -> None: + self._service_id = service_id + + def __call__( + self, method: Callable[Concatenate[T_Container, P], R] + ) -> Callable[[T_Container], Callable[P, R]]: + new_method = _factory_to_factory_provider(method) + return service(self._service_id)(new_method) + + +def autoid_factory_service( + method: Callable[Concatenate[T_Container, P], R], +) -> Callable[[T_Container], Callable[P, R]]: + """A decorator to create a factory service, with an automatically generated service id. + + Decorate a method that takes any number of arguments and returns an object. The resulting + service will be that factory - taking the same arguments and returning a new object each time. + """ + new_method = _factory_to_factory_provider(method) + return autoid_service(new_method) + + def autoid(method: Callable[..., T_ServiceType]) -> ServiceId[T_ServiceType]: """ Get a service id for a method. diff --git a/rats-apps/test/python/rats_test/apps/example/_dummy_containers.py b/rats-apps/test/python/rats_test/apps/example/_dummy_containers.py index 2188755c..f78c4e11 100644 --- a/rats-apps/test/python/rats_test/apps/example/_dummy_containers.py +++ b/rats-apps/test/python/rats_test/apps/example/_dummy_containers.py @@ -1,3 +1,4 @@ +from typing import Callable from rats import apps from ._dummies import ITag, Tag @@ -9,6 +10,7 @@ class _PrivateIds: C1S1C = apps.ServiceId[ITag]("c1s1c") C1S2A = apps.ServiceId[ITag]("c1s2a") C1S2B = apps.ServiceId[ITag]("c1s2b") + TAG_FACTORY_1 = apps.ServiceId[Callable[[str], ITag]]("tag-factory-1") class DummyContainer1(apps.Container): @@ -47,6 +49,14 @@ def c2s1a(self) -> ITag: # Calling a service from another container using its public service id. return self._app.get(DummyContainerServiceIds.C2S1B) + @apps.factory_service(_PrivateIds.TAG_FACTORY_1) + def tag_factory_1(self, ns: str) -> ITag: + return Tag(ns) + + @apps.autoid_factory_service + def tag_factory_2(self, ns: str) -> ITag: + return Tag(ns) + class DummyContainer2(apps.Container): _app: apps.Container @@ -85,3 +95,6 @@ class DummyContainerServiceIds: C2S1A = apps.autoid(DummyContainer1.c2s1a) C2S1B = apps.autoid(DummyContainer2.unnamed_service) + + TAG_FACTORY_1 = _PrivateIds.TAG_FACTORY_1 + TAG_FACTORY_2 = apps.autoid(DummyContainer1.tag_factory_2) diff --git a/rats-apps/test/python/rats_test/apps/test_service_caching.py b/rats-apps/test/python/rats_test/apps/test_service_caching.py index 3c44e37f..1f36b7e5 100644 --- a/rats-apps/test/python/rats_test/apps/test_service_caching.py +++ b/rats-apps/test/python/rats_test/apps/test_service_caching.py @@ -62,3 +62,31 @@ def test_caching_of_service_calls_between_containers(self) -> None: assert c2s1a is c2s1b assert c2s1a.get_tag() == "c2.s1" + + def test_caching_of_factory_services(self) -> None: + f1 = self._app.get(DummyContainerServiceIds.TAG_FACTORY_1) + f1_ = self._app.get(DummyContainerServiceIds.TAG_FACTORY_1) + f2 = self._app.get(DummyContainerServiceIds.TAG_FACTORY_2) + f2_ = self._app.get(DummyContainerServiceIds.TAG_FACTORY_2) + + # multiple calls using the same service should return the same factory object + assert f1 is f1_ + assert f2 is f2_ + + # but calls using different service ids should return different objects + assert f1 is not f2 + + # and multiple calls to the factories themselves should return different objects + t1a = f1("f1") + t1b = f1("f1") + t2a = f2("f2") + t2b = f2("f2") + + assert t1a is not t1b + assert t2a is not t2b + + assert t1a.get_tag() == "f1" + assert t1b.get_tag() == "f1" + + assert t2a.get_tag() == "f2" + assert t2b.get_tag() == "f2"