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).
Mast
13.8k12 gold badges56 silver badges127 bronze badges
asked Jul 20, 2017 at 0:51
\$\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.

answered Jan 2, 2023 at 20:09
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.