Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

python

django

I'm trying to inject dependencies into my Django view (controller?). Here's some background.

Normally, the urls.py file is what handles the routing. It is usually something like this:

 urlpatterns = [
     path("", views.get_all_posts, name="get_all_posts"),
     path("<int:post_id>", views.get_post, name="get_post"),
     path("create", views.create_post, name="create_post"),
 ]

The problem with this, is that once you get to create_post for instance, you might have a dependency on a service that creates posts:

# views.py
...

def create_post(self):
    svc = PostCreationService()
    svc.create_post()

This kind of pattern is difficult to test. While I know python testing libraries have tools to mock this sort of thing, I'd rather inject the dependency into the view. Here's what I came up with.

A Controller class that has a static method, export(deps) that takes in a list of dependencies and returns a list of url pattern objects:

class ApiController(object):

    @staticmethod
    def export(**deps):
        ctrl = ApiController(**deps)
        return [
            path("", ctrl.get_all_posts, name="get_all_posts"),
            path("<int:post_id>", ctrl.get_post, name="get_post"),
            path("create", ctrl.create_post, name="create_post"),
        ]

    def __init__(self, **deps):
        self.deps = deps

    def get_all_posts():
        pass
    ...

This looks janky, but I'm not aware of any other way to do what I'm trying to do. The controller needs to return a list of url patterns, and it also needs to take in a list of dependencies. Using the above technique, I can do this in urls.py:

urlpatterns = ApiController.export(foo_service=(lambda x: x))

I am now free to use foo_service in any of the methods of ApiController.

Note:

One alternative would be for the constructor to return the list of urls, but I don't see that as a huge improvement over this. In fact, it strikes me as being more confusing because the class constructor would return a list instead of an instance of the class.

Note 2:

I'm aware that python has mocking tools for mocking class members. Please don't suggest using them. I'd like to use DI as the way to control and manage dependencies.

Any ideas on what the best way to do this is?

like image 702
dopatraman Avatar asked Jun 11 '19 01:06

dopatraman


1 Answers

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
like image 89
rabbit.aaron Avatar answered Oct 19 '22 14:10

rabbit.aaron