Jivago provides a powerful dependency injection engine as a means of implementing inversion of control.
Classes annotated with
@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.
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.
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 # ...
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
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)
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_all methods for requesting components.