[Django]-Is this the right way to do dependency injection in Django?

15👍

Consider injecting using decorators:

from functools import wraps

class ServiceInjector:

    def __init__(self):
        self.deps = {}

    def register(self, name=None):

        name = name
        def decorator(thing):
            """
            thing here can be class or function or anything really
            """

            if not name:
                if not hasattr(thing, "__name__"):
                    raise Exception("no name")
                thing_name = thing.__name__
            else:
                thing_name = name
            self.deps[thing_name] = thing
            return thing

        return decorator

    def inject(self, func):

        @wraps(func)
        def decorated(*args, **kwargs):
            new_args = args + (self.deps, )
            return func(*new_args, **kwargs)

        return decorated

# usage:


si = ServiceInjector()

# use func.__name__, registering func
@si.register()
def foo(*args):
    return sum(args)


# we can rename what it's been registered as, here, the class is registered 
# with name `UpperCase` instead of the class name `UpperCaseRepresentation`
@si.register(name="UpperCase")
class UpperCaseRepresentation:
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return self.value.upper()

#register float
si.register(name="PI")(3.141592653)


# inject into functions
@si.inject 
def bar(a, b, c, _deps): # the last one in *args would be receiving the dependencies
    UpperCase, PI, foo = _deps['UpperCase'], _deps['PI'], _deps['foo']
    print(UpperCase('abc')) # ABC
    print(PI) # 3.141592653
    print(foo(a, b, c, 4, 5)) # = 15

bar(1, 2, 3)

# inject into class methods
class Foo:

    @si.inject
    def my_method(self, a, b, _deps, kwarg1=30):
        return _deps['foo'](a, b, kwarg1)

print(Foo().my_method(1, 2, kwarg1=50)) # = 53

14👍

You could take a look at https://github.com/ets-labs/python-dependency-injector, but that is a pretty big setup.

You could also create something small like a Service factory

# services.py
class ServiceFactory:
    def __init__(self):
        self.__services = {}

    def register(self, name, service_class):
        # Maybe add some validation
        self.__services[name] = service_class

    def create(self, name, *args, **kwargs):
        # Maybe add some error handling or fallbacks
        return self.__services[name](*args, **kwargs)

factory = ServiceFactory()


# In your settings.py for example
from services import factory
factory.register('post_creation', PostCreationService)


# Or maybe in apps.ready do auto_load that will loop all apps and get config from services.py


# In your views.py
from services import factory

def create_post(self):
    svc = factory.create('post_creation')
    svc.create_post()


# In your tests.py
from services import factory

def setUp(self):
    factory.register('post_creation', FakePostCreationService)
👤Krukas

6👍

While reading Dependency Injection Principles, Practices, and Patterns and trying to apply the examples to a django app I came up with the following:

# views.py

class IndexView(View):
    # Must include this to bypass django's validation
    product_service: IProductService = None
    
    # Init method not necessary but more explicit
    def __init__(self, product_service: IProductService):
        self.product_service = product_service

    def get(self, request):
        self.product_service.do_stuff()
        ...

# urls.py

# Construct dependencies. I guess this is the closest to the entry-point we can get
# with Django.
repo = DjangoProductRepository()
product_service = ProductService(repo)

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", 
         IndexView.as_view(product_service=product_service),
         name="index"),
]

3👍

This is only an updated version of rabbit.aaron reply above. My idea is to be able to specify which dependencies to inject instead of getting a dictionary with all registered dependencies.

from functools import wraps

class ServiceInjector:
    deps = {}

    def register(self, name=None):
        name = name

        def decorator(thing):
            """
            thing here can be class or function or anything really
            """

            if not name:
                if not hasattr(thing, '__name__'):
                    raise Exception('no name')
                thing_name = thing.__name__
            else:
                thing_name = name
            self.__class__.deps[thing_name] = thing
            return thing

        return decorator

    class inject:
        def __init__(self, *args):
            self.selected_deps = args

        def __call__(self, func):
            @wraps(func)
            def decorated(*args, **kwargs):
                selected_deps = {k: v for k, v in ServiceInjector.deps.items() if k in self.selected_deps}
                new_kwargs = {**kwargs, **selected_deps}
                return func(*args, **new_kwargs)

            return decorated

Usage:

si = ServiceInjector()

# use func.__name__, registering func
@si.register()
def foo(*args):
    return sum(args)

Custom naming still works

@si.register(name='uppercase')
class UpperCaseRepresentation:
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return self.value.upper()

Register float

si.register(name="PI")(3.141592653)

Inject into functions

@si.inject('foo', 'PI', 'uppercase')
def bar(a, b, c, uppercase: UpperCaseRepresentation, **kwargs):
    """
    You can specify dependencies as keyword arguments and add typehint annotation.
    """
    UpperCase, foo = kwargs['UpperCase'], kwargs['foo']
    print(uppercase('abc')) # ABC
    print(PI) # 3.141592653
    print(foo(a, b, c, 4, 5)) # = 15

bar(1, 2, 3)

Inject into class methods

class Bar:
    @si.inject('foo')
    def my_method(self, a, b, foo, kwarg1=30):
        return foo(a, b, kwarg1)

print(Bar().my_method(1, 2, kwarg1=50)) # = 53

0👍

You could go the flask route and export a class instance with a property that initializes and caches the service on first access. E.g:

def default_factory():
    pass

# service.py
class ServiceProvider:
    def __init__(self, create_instance=default_factory):
        self.create_instance = create_instance

    _instance = None

    @property
    def service(self):
       if self._instance:
           return self._instance
       self._instance = self.create_instance()
       return self._instance

service_provider = ServiceProvider()
from .service import service_provider

# views.py
def view(request):
    service_provider.service.do_stuff()
    # etc.

This has the advantages of being easy to mock and not having any magic.

👤Maus

0👍

The most boring solution I could come up with involves using class variables:

# Module services.post_service
def default_create_post():
    return "foo"

class Provider:
    create_post = default_create_post

Then you could import and use normally in a view or elsewhere:

from services import post_service

post_service.Provider.create_post()
# Should return "foo"

And when testing it could be swapped out before being called:

from django.test import TestCase
from services import post_service
from unittest.mock import patch

class MyTestCase(TestCase):

    @patch('services.post_service.default_create_post')
    def test_some_view(self, mock_create_post):
        mock_create_post.return_value = "bar"
        post_service.Provider.create_post = mock_create_post
        # Now when calling post_service.Provider.create_post it should just return "bar"

Leave a comment