Skip to content

Experimental Lagom Features

Lagom provides a module with code which is not yet ready for a stable release. This code can be considered beta. It works and is tested but the interface may change based on feedback after usage.

Django container

Overview

The django integration is designed assuming there will be one container per django app. So the starting point is a new file dependency_config.py alongside your existing views.py and urls.py.

# dependency_config.py
# a per app dep injection container

container = Container()
container[SomeService] = SomeService("connection details etc")

dependencies = DjangoIntegration(container)

and then in views.py:

from .dependency_config import dependencies


@dependencies.bind_view
def index(request, dep: SomeService):
    return HttpResponse(f"service says: {dep.get_message()}")

# Or if you prefer class based views

@dependencies.bind_view
class CBVexample(View):
    def get(self, request, dep: SomeService):
        return HttpResponse(f"service says: {dep.get_message()}")

These views can then be used as normal in the url definitions:

from django.urls import path

from . import views


urlpatterns = [
    path('function_view', views.index, name='func_view'),
    path('class_based', views.CBVexample.as_view(), name='cbv'),
]

Django models

Django models are usually referenced statically. Some extra code is provided to make these injectable instead.

When defining the container, list all the models that should be available via the container:

# dependency_config.py
# a per app dep injection container
from .models import Question

container = Container()

dependencies = DjangoIntegration(container, models=[Question])

Now in the views you can use:

from django.http import HttpResponse
from django.utils import timezone
from lagom.experimental.integrations.django import DjangoModel

from .dependency_config import dependencies
from .models import Question


@dependencies.bind_view
def new_question(request, questions: DjangoModel[Question]):
    new_question = questions.new(question_text="What's next?", pub_date=timezone.now())
    new_question.save()
    return HttpResponse(f"new question created")


@dependencies.bind_view
def question_count(request, questions: DjangoModel[Question]):
    count = questions.objects.all().count()
    return HttpResponse(f"{count} questions are in the DB")

in these examples questions.objects is the same as calling Question.objects and questions.new() is the same as Question(). The benefit now though is the view function has no global state dependency. We can call the view functions directly in tests passing in whatever we want to questions. This enables the dependency on the DB to be switched out without any monkey patching at all.

Django settings

A class DjangoSettings is provided. When bound to the container this will automatically get injected with the settings of your app.

# settings.py
SECRET_MSG = "hello world"

# views.py
@dependencies.bind_view
def show_secret_message(request, settings: DjangoSettings = injectable):
    return HttpResponse(f"your secret message is: {settings.SECRET_MSG}")

Injecting Request

If any argument type hints on HttpRequest then the django object for the current request will be injected.

# some_services.py
class DataStoreForRequest:
    def __init__(self, request: HttpRequest, db: SomeDb):
        # Now we can use anything generic we want from the request
        pass

# views.py
@dependencies.bind_view
def question_count(request, store: DataStoreForRequest = injectable):
    # The `store` here was constructed with the correct request object
    return HttpResponse(f"Done")

Click

An integration layer is provided which wraps click's decorators and wraps the function with lagom injection.

container = Container()
cli = ClickIntegration(container)


@cli.command()
@cli.argument("name")
def call_the_thing(input, service: SomeServiceClass = injectable):
    service.do_thing(input)

to enable testing without monkey patching the integration ends the Command class to retain a reference to the function as plain_function:

def test_something():
    mock_service = mock.create_autospec(SomeServiceClass)
    call_the_thing.plain_function("something", mock_io)
    mock_service.do_thing.assert_called_once_with("something")

in addition, for testing echoed output a ClickIO class is automatically made available:

@cli.command()
@cli.argument("name")
def hello(name, io: ClickIO = injectable):
    io.echo(f"Hello {name}")

Flask Blueprints

The integration has the same interface as for apps:

from lagom.experimental.integrations.flask import FlaskBlueprintIntegration

simple_page = Blueprint('simple_page', template_folder='templates')
simple_page_with_deps = FlaskBlueprintIntegration(simple_page, container)

@simple_page_with_deps.route("/save_it/<string:thing_to_save>", methods=['POST'])
def save_to_db(thing_to_save, db: Database = injectable):
    db.save(thing_to_save)
    return 'saved'