Dependency Injection

Jivago provides a powerful dependency injection engine as a means of implementing inversion of control.

Basic Usage

Classes annotated with @Component or @Resource are automatically registered in the built-in service locator. Dependencies are constructor-injected, and require proper typing hints to be used.

from jivago.inject.annotation import Component
from jivago.lang.annotations import Inject
from jivago.wsgi.annotations import Resource


@Component
class CalculatorClass(object):

    def do_calculation(self) -> int:
        return 4


@Resource("/calculation")
class CalculatedResource(object):

    @Inject
    def __init__(self, calculator: CalculatorClass):
        self.calculator = calculator

Always make sure that the type hint corresponds exactly to the requested object. (i.e. The type annotation could be used directly as a constructor.) An identically-named, but otherwise different class will not work.

Collections

Using a collection type hint, all children of a class can be requested. Take a look at the following example :

import random
from typing import List

from jivago.inject.annotation import Component
from jivago.lang.annotations import Override, Inject


class Calculator(object):

    def do_calculation(self, input: int) -> int:
        raise NotImplementedError


@Component
class ConstantCalculator(Calculator):

    @Override
    def do_calculation(self, input: int) -> int:
        return 5


class RandomCalculator(Calculator):

    @Override
    def do_calculation(self, input: int) -> int:
        return random.randint(0, 100)


@Component
class CalculationService(object):

    @Inject
    def __init__(self, calculators: List[Calculator]):
        self.calculators = calculators

    def calculate(self, input: int) -> List[int]:
        return [calculator.do_calculation(input) for calculator in self.calculators]

The CalculationService class is injected with a list of all components which implement the Calculator interface.

Scopes

By default, all components are re-instantiated when a request is received. However, a @Singleton annotation is provided for when unicity is important. (e.g. when making a simple persistence mechanism held in memory.)

from typing import List

from jivago.inject.annotation import Component, Singleton


@Component
@Singleton
class InMemoryMessageRepository(object):

    def __init__(self):
        self.content = []

    def save(self, message: str):
        self.content.append(message)

    def get_messages(self) -> List[str]:
        return self.content

A singleton component will be instantiated when it is first requested, and reused for subsequent calls.

Jivago also provides the @RequestScoped annotation for components which should be re-used for the lifetime of a single HTTP request. Instances will be destroyed after the resource class returns and the filter chain is unwound. Using request-scoped components outside of an HTTP request lifecycle (e.g. async event bus, background worker, init hooks …) is not supported and may lead to unexpected results.

from jivago.inject.annotation import Component, RequestScoped
from jivago.lang.annotations import Inject, Override
from jivago.wsgi.annotations import Resource
from jivago.wsgi.filter.filter import Filter
from jivago.wsgi.filter.filter_chain import FilterChain
from jivago.wsgi.request.request import Request
from jivago.wsgi.request.response import Response


@Component
@RequestScoped
class UserSession(object):
    """A single instance will be shared across the request lifecycle,
    from the filter chain to the resource class and any synchronous call it makes."""

    def __init__(self):
        self.user_id = None

    def set(self, user_id: str):
        self.user_id = user_id

    def get(self) -> str:
        return self.user_id


@Component
class UserSessionInitializationFilter(Filter):

    @Inject
    def __init__(self, session: UserSession):
        self.session = session

    @Override
    def doFilter(self, request: Request, response: Response, chain: FilterChain):
        self.session.set(request.headers["Authorization"])
        chain.doFilter(request, response)


@Resource("/")
class MyResourceClass(object):

    @Inject
    def __init__(self, user_session: UserSession):
        # This is the same instance that was initialized in the request filter class.
        self.user_session = user_session
    
    # ...

Factory Functions

When complex scoping is required for a given component, for example when handling a database connection, factory functions can be used to instantiate and cache components using the @Provider annotation. In this case, the return type hint defines the class to which the function is registered.

from jivago.inject.annotation import Provider, Singleton


class DatabaseConnection(object):

    def __init__(self):
        # open a connection, etc.
        pass

    def query_database(self) -> int:
        # use the opened connection, etc.
        return 5


connection = None


@Provider
def get_database_connection() -> DatabaseConnection:
    global connection
    if connection is None:
        connection = DatabaseConnection()
    return connection


@Provider
@Singleton
def get_singleton_bean(my_dependency: Dependency) -> MySingletonBean:
    # Will only be called once
    return Dependency(...)

The provider function can take any registered component as arguments. By adding @Singleton to the provider function, it will be lazily instantiated only once, thereby exhibiting the same behaviour as components.

Manual Component Registration

When fine-tuned control is necessary, the service locator should be manually configured by extending the Context object. In order to do so, first override either ProductionJivagoContext or DebugJivagoContext. This will be your new application context, which should be passed to the JivagoApplication object. The configure_service_locator is where component registration is done. Use the self.serviceLocator.bind method to manually register components. Note that Jivago decorators will not be taken into consideration when using manual component registration.

from jivago.config.production_jivago_context import ProductionJivagoContext
from jivago.lang.annotations import Override


class MyApplicationContext(ProductionJivagoContext):

    @Override
    def configure_service_locator(self):
        super().configure_service_locator()
        self.serviceLocator.bind(MessageRepository, InMemoryMessageRepository)

The bind(interface, implementation) methods registers an implementation to its interface. The service locator acts as a dictionary, where the interface is the key, and the implementation is the value. The interface should always be a class.

The implementation can be any of the following :
  • A class

  • An instance of a class

  • A function which, when called, returns an instance of a class

When a class is given, the default behaviour is applied : a new instance is created whenever the interface is requested. Registering an instance of the class causes it to act as a singleton. Finally, a registered function will be invoked whenever the interface class is requested.

Service Locator Object

Similarily, components can be manually requested by directly invoking the ServiceLocator object. A reference to the ServiceLocator object can be obtained either through dependency injection, or statically.

from jivago.config.abstract_context import AbstractContext
from jivago.inject.annotation import Component
from jivago.inject.service_locator import ServiceLocator
from jivago.lang.annotations import Inject


@Component
class Calculator(object):
    def do_calculation(self) -> int:
        return 5

# ServiceLocator injection
@Component
class CalculationService(object):

    @Inject
    def __init__(self, service_locator: ServiceLocator):
        self.service_locator = service_locator
        self.calculator = self.service_locator.get(Calculator)


# Static access to the ServiceLocator object from anywhere
def calculate() -> int:
    service_locator = AbstractContext.INSTANCE.service_locator()
    calculator = service_locator.get(Calculator)
    return calculator.do_calculation()

The service locator has get and get_all methods for requesting components.