7
\$\begingroup\$

so, first of all, I've been into trying to have a simple dependency container as possible in python and with your help managed to get conceive https://github.com/dareenzo/tinydic, many thanks.

Now, I use the Flask for my apps, and from by background with other languages I'm used to the idea of injecting dependencies into my controllers with the typical MVC frameworks. Flask advocates for using simple functions to handle the requests, rather than classes (although it supports class based views). Those functions are invoked by Flask dynamically upon receiving web requests and there's not much I could do with them in what regards to passing stuff for them. (Maybe I could use some globals like g or current_app inside the view to get the dependencies I need, but I don't like it ... feels like using a Service Locator and taking instead of asking).

Luckily though 2 things occurred to me recently

    • Python has decorators that allow me to "modify" functions

So, I came with this idea, that

  1. we just write the view functions taking the dependencies we want to inject first and with type hints.
  2. we put a decorator on the views that will read those type hints and using partial application modify the function to have the dependencies inject and just return a view with just enough params to handle the HTTP request.

POC code is bellow and I'd really appreciate your feedback

from functools import partial
import inspect
from flask import Flask


class Container(object):
    def __init__(self):
        self._registry = {}

    def register(self, key, value, constant=False):
        if constant:
            self._registry[key] = lambda container: value
        else:
            self._registry[key] = value

    def inject(self, func):
        func_name = func.__name__
        type_hints = zip(func.__annotations__.keys(), func.__annotations__.values())
        for parameter_name, parameter_type in type_hints:
            if parameter_name == 'return': break
            # NOTE: Assumes we're doing the right thing: depending on the interface and not the implementation
            parameter_instance = self._registry.get(parameter_type.__qualname__)(self)
            func = partial(func, parameter_instance)
        func.__name__ = func_name  # NOTE: required by Flask
        return func


class UpperCaseFormatter:
    def __init__(self):
        pass

    def format(self, string):
        return string.upper()

# Composition root
# ---
container = Container()
container.register(UpperCaseFormatter.__qualname__, UpperCaseFormatter(), True)


# Web app
# ---
app = Flask(__name__)

@app.route('/<name>')
@container.inject # NOTE: must come first so final view version with dependencies already injected is pass to Flask's app.route
def index_action(formatter: UpperCaseFormatter, name):
    return formatter.format(name)


app.run(debug=True)

initial considerations on advantages:

  • the required dependencies to handle the request are explicitly declared
  • we obey the principle of ask don't take (from g or current_app)

Thanks

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy