Skip to content

Commit

Permalink
✨ feat(rats.apps): New decorators factory_service and autoid_factory_…
Browse files Browse the repository at this point in the history
…service simplify creating services that are factories.
  • Loading branch information
elonp committed Sep 5, 2024
1 parent 082f7ef commit e4183ff
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 1 deletion.
4 changes: 4 additions & 0 deletions rats-apps/src/python/rats/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
fallback_service,
group,
service,
factory_service,
autoid_factory_service,
)
from ._composite_container import CompositeContainer
from ._container import (
Expand Down Expand Up @@ -63,4 +65,6 @@
"StandardRuntime",
"StandardRuntime",
"SimpleApplication",
"factory_service",
"autoid_factory_service",
]
53 changes: 52 additions & 1 deletion rats-apps/src/python/rats/apps/_annotations.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -8,6 +8,8 @@
from ._scoping import scope_service_name

P = ParamSpec("P")
R = TypeVar("R")
T_Container = TypeVar("T_Container")


def service(
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions rats-apps/test/python/rats_test/apps/example/_dummy_containers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Callable
from rats import apps

from ._dummies import ITag, Tag
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
28 changes: 28 additions & 0 deletions rats-apps/test/python/rats_test/apps/test_service_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit e4183ff

Please sign in to comment.