diff --git a/src/anemic/ioc/__init__.py b/src/anemic/ioc/__init__.py index c8e5c13..ab5f747 100644 --- a/src/anemic/ioc/__init__.py +++ b/src/anemic/ioc/__init__.py @@ -1 +1,2 @@ from .container import IOCContainer +from .autowired import autowired diff --git a/src/anemic/ioc/autowired.py b/src/anemic/ioc/autowired.py new file mode 100644 index 0000000..aeff649 --- /dev/null +++ b/src/anemic/ioc/autowired.py @@ -0,0 +1,36 @@ +from typing import Any, TypeVar, cast + +from ..decorators import reify_attr + +__all__ = [ + "autowired", +] + +T = TypeVar("T") + + +class SentinelType(Any): + pass + + +def autowired( + interface: type[T] = SentinelType, + *, + name: str = "", + context: Any = None, +) -> T: + # Default for IOCContainer.get is object + if interface is SentinelType: + interface = cast(type[T], object) + + @reify_attr[T] + def getter(self) -> T: + return self.container.get( + interface=interface, + name=name, + context=context, + ) + + # TODO: cast to T from reify_attr[T], because PyCharm + # doesn't understand types of custom descriptors + return cast(T, getter) diff --git a/tests/anemic_test/ioc/test_autowired.py b/tests/anemic_test/ioc/test_autowired.py new file mode 100644 index 0000000..a0a60f6 --- /dev/null +++ b/tests/anemic_test/ioc/test_autowired.py @@ -0,0 +1,65 @@ +import pytest + +from anemic.ioc.autowired import autowired +from anemic.ioc.container import FactoryRegistry, IOCContainer + + +class ServiceA: + times_inited = 0 + + def __init__(self, container): + ServiceA.times_inited += 1 + self.container = container + + def foo(self): + return "foo" + + +class ServiceB: + a = autowired(ServiceA) + + def __init__(self, container): + self.container = container + + def foobar(self): + return self.a.foo() + "bar" + + +@pytest.fixture(autouse=True) +def setup(): + ServiceA.times_inited = 0 + + +@pytest.fixture +def registry(): + ret = FactoryRegistry("application") + ret.register( + interface=ServiceA, + factory=ServiceA, + ) + ret.register( + interface=ServiceB, + factory=ServiceB, + ) + return ret + + +@pytest.fixture +def container(registry): + return IOCContainer(registry) + + +def test_autowired_attribute_access(container: IOCContainer): + assert ServiceA.times_inited == 0 # sanity + b = container.get(interface=ServiceB) + assert ServiceA.times_inited == 0 # ServiceA not initialized yet + a = b.a + assert ServiceA.times_inited == 1 # ServiceA initialized after b.a accessed + assert isinstance(a, ServiceA) + assert a is b.a # same instance + assert ServiceA.times_inited == 1 # ServiceA not initialized again + + +def test_autowired_method_call(container: IOCContainer): + b = container.get(interface=ServiceB) + assert b.foobar() == "foobar"