15
\$\begingroup\$

I've been into trying to have an as simple as possible dependency container in Python and with your help managed to conceive TinyDIC, many thanks.

Now, I use 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 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 below 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).
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

It would be an aid to the Gentle Reader to offer a # comment or """docstring""" explaining register's final boolean. The demo code suggests that callers would usually set it True. Maybe switch the default value?


I don't understand the motivation for this line:

        type_hints = zip(func.__annotations__.keys(), func.__annotations__.values())

Surely it is just this, right?

        type_hints = func.__annotations__.items()

I do rather like the way we go from type hint (roughly, a comment) to something we actually call to construct the 200 document.

Not sure I'd want to turn a team of a dozen engineers loose with this technique, though. There might be a bit too much automagic in it.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.