Skip to content

Latest commit

 

History

History
329 lines (219 loc) · 8.41 KB

README.rst

File metadata and controls

329 lines (219 loc) · 8.41 KB

build status Pypi

YADI

Yet Another Dependency Injection framework

YADI is a dependency injection framework. It supports both classes and function in a declarative fashion.

Installation

pip install yadi-framework

Basic examples

This is a simple injection example:

from yadi.context_impl import DEFAULT_CONTEXT
from yadi.decorators import inject
from yadi.types import Yadi


@inject()
class Component1:
    pass


@inject()
class Component2:
    def __init__(self, c1: Yadi[Component1]):
        self.c1 = c1


@inject()
class Component3:
    def __init__(self, c1: Yadi[Component1]):
        self.c1 = c1


c2 = DEFAULT_CONTEXT.get_bean(Component2)  # type: Component2
c3 = DEFAULT_CONTEXT.get_bean(Component3)  # type: Component3
print(Component1 is type(c2.c1))  # True
print(c2.c1 is c3.c1)  # True

Here it is an example of how to inject functions:

from yadi.context_impl import DEFAULT_CONTEXT
from yadi.decorators import inject
from yadi.types import Yadi


@inject()
class Component:
    pass


@inject(name='another_function')
def h(x, y, z=None):
    assert isinstance(x, Component)
    print('Function h:', type(x))


@inject(name='my_function')
def f(a: Yadi[Component], b, c: Yadi['another_function'] = None, d: str = None):
    c(a, b, z=d)


DEFAULT_CONTEXT.get_bean('my_function')(23, d=5)  # Function h: <class '__main__.Component'>

Scopes

By default, all the beans are saved as Singleton. Each singleton is stored in its context, that is, there is a single instance for each context instance.

Alternatively, it is possible to save beans as Prototypes, that is, a different instance is generated whenever the bean is referred to.

from yadi import context
from yadi import types
from yadi.context_impl import DEFAULT_CONTEXT
from yadi.decorators import inject


@inject(scope=context.PROTOTYPE, name='a component 1')
class Component1:
    pass


@inject(name='a component 2')
class Component2:
    def __init__(
            self,
            f1: types.Yadi[Component1],
            f2: types.Yadi['a component 1']):
        self.f1, self.f2 = f1, f2


@inject(name='a component 3')
class Component3:
    def __init__(
            self,
            f1: types.Yadi[Component1],
            f2: types.Yadi['a component 1']):
        self.f1, self.f2 = f1, f2


c2 = DEFAULT_CONTEXT.get_bean('a component 2')  # type: Component2
c3 = DEFAULT_CONTEXT.get_bean('a component 3')  # type: Component3

print(isinstance(c2.f1, Component1))  # True
print(isinstance(c2.f2, Component1))  # True

print(isinstance(c3.f1, Component1))  # True
print(isinstance(c3.f2, Component1))  # True

print(c2.f1 == c2.f2)  # False
print(c3.f1 == c3.f2)  # False
print(c2.f1 == c3.f1)  # False
print(c2.f1 == c3.f2)  # False
print(c2.f2 == c3.f1)  # False
print(c2.f2 == c3.f2)  # False

It is also possible to define custom scopes and add them to a context.

Here it is an example of thread-local scope:

import threading

from yadi.context import Scope
from yadi.context_impl import DEFAULT_CONTEXT
from yadi.decorators import inject


class ThreadLocalScope(Scope):
    def __init__(self):
        self._tl = threading.local()

    def get(self, key: str):
        return getattr(self._tl, key, None)

    def set(self, key: str, obj: object):
        setattr(self._tl, key, obj)

    @property
    def name(self):
        return 'threadlocal'

    @property
    def level(self):
        return 100


DEFAULT_CONTEXT.add_scope(ThreadLocalScope())


@inject(scope='threadlocal', name='a component 1')
class Component1:
    pass


c1 = DEFAULT_CONTEXT.get_bean('a component 1')
c1_2 = DEFAULT_CONTEXT.get_bean('a component 1')

thread_c1 = []
c1_t = None


def _f():
    global c1_t
    c1_t = DEFAULT_CONTEXT.get_bean('a component 1')
    print(c1_t == DEFAULT_CONTEXT.get_bean('a component 1'))  # True
    thread_c1.append(c1_t)


t = threading.Thread(target=_f)
t.start()
t.join()

print(c1 == c1_2)  # True
print(c1 == c1_t)  # False

Scoped proxies

Let's suppose to inject a thread-local scoped bean in a singleton. As a result, different thread sharing the same singleton should not share the same thread local bean, which is not possible.

In order to solve this issue, YADI creates a proxy around the injected bean that delegates any access to the current bean in the context.

More in general, scopes have a level attribute: if the injected bean has a higher scope level than the container bean, the injected bean is wrapped into a scoped proxy.

Here it is an example of scoped proxies (don't worry, you do not have to do anything to make it work).

import random
import threading

from yadi.bean_factories import _ScopedProxy
from yadi.context import Scope
from yadi.context_impl import DEFAULT_CONTEXT
from yadi.decorators import inject
from yadi.types import Yadi


class ThreadLocalScope(Scope):
    def __init__(self):
        self._tl = threading.local()

    def get(self, key: str):
        return getattr(self._tl, key, None)

    def set(self, key: str, obj: object):
        setattr(self._tl, key, obj)

    @property
    def name(self):
        return 'threadlocal'

    @property
    def level(self):
        return 100


DEFAULT_CONTEXT.add_scope(ThreadLocalScope())


@inject(scope='threadlocal')
class Component1:
    def __init__(self):
        self.object_id = random.randint(0, 1000000)


@inject(name='a component')
class Component2:
    def __init__(self, f1: Yadi[Component1]):
        self.f1 = f1


component = DEFAULT_CONTEXT.get_bean('a component')
component_thread_id = []
print('Main thread, scoped proxy type', type(component.f1) == _ScopedProxy)  # Main thread, scoped proxy type True


def _f():
    component_thread = DEFAULT_CONTEXT.get_bean('a component')
    print('Subthread, scoped proxy type', type(component_thread.f1) == _ScopedProxy)
    component_thread_id.append(component_thread.f1.object_id)
    print(
        'Subthread, bean id',
        component_thread.f1.object_id == DEFAULT_CONTEXT.get_bean('a component').f1.object_id)


t = threading.Thread(target=_f)
t.start()
t.join()  # Subthread, scoped proxy type True
          # Subthread, bean id True

print('Main thread, bean id', component.f1.object_id == component_thread_id[0])  # Main thread, bean id False

Contexts

All the components are kept in a context.

By default, the inject decorator keeps the beans instances in yadi.context_impl.DEFAULT_CONTEXT.

You might want to instantiate a new context and pass it as a context keyword argument of inject decorator.

Life cycle

It is possible to trigger beans whenever one of them is created. In order to define the method(s) to trigger, it is necessary to decorated them with post_create, as follows:

from yadi.context_impl import DEFAULT_CONTEXT
from yadi.decorators import inject, post_create
from yadi.types import Yadi


@inject()
class Component1:
    pass

@inject()
class Component2:
    def __init__(self, c1: Yadi[Component1]):
        self.c1 = c1
        self.invoked_post_create = 0

    @post_create
    def finished_creating(self):
        print('Component 1:', self.c1)  # Component 1: <__main__.Component1 object at 0x7f42e90d2e48>
        self.invoked_post_create += 1


component_2 = DEFAULT_CONTEXT.get_bean(Component2)  # type: Component2
print('post_create invokations:', component_2.invoked_post_create)  # post_create invokations: 1
DEFAULT_CONTEXT.get_bean(Component2)
print('post_create invokations:', component_2.invoked_post_create)  # post_create invokations: 1