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:
- we just write the view functions taking the dependencies we want to inject first and with type hints.
- 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
orcurrent_app
).