Experimenting how to write easily a small self contained web application to deploy in seconds on any machine i started studying Python frameworks, like Flask and Bottle. I discovered then that there is not much fuss about Dependency Injection and that organizing Bottle or Flask apps is pretty hard (especially if you come from Java/C#/C++ as i do)
I started then approaching various problems
If you want you can peek directly at the github project
I had not found many things in Python about DI, the most interesting thing i founded is autowired, a project able to scan modules (mostly) and auto load the dependencies.
To install it just call
pip install autowired
Then you have to create an "ApplicationContext", extenting "Context" an autowired specific item that will contains all the logic for the autowiring.
Every class that has to be autowired should be annotated with component
@component class MyClass:
You could then load the module to autowire
class ApplicationContext(Context): ... ctx = ApplicationContext() ctx.container.component_scan(your_root_module)
BUT this is not enough! This should scan recursively but i founded that i had to hack the thing a bit. With this trick all the modules are scanned recursively and loaded one by one. With this approach i was finally able to load -everything-
Module = type(sys) class ApplicationContext(Context): def load_modules_in_dir(self, package_dir): packageDirName = package_dir.replace("/", ".").replace("...", "") for (_, module_name, is_pkg) in pkgutil.iter_modules([package_dir]): # import the module and iterate through its attributes try: module = import_module(f"{packageDirName}.{module_name}") self.container.component_scan(module) except: continue finally: continue def load_submodules(self, package_dir): for subdir, dirs, files in os.walk("../" + package_dir): if not subdir.endswith("_"): self.load_modules_in_dir(subdir) def __init__(self, root_module: Module): self.load_submodules(str(root_module.__path__._name))
All the components are created as singletons with
mycomponent: CompType = ctx.container.resolve(CompType)
And i wrapped this call inside my ApplicationContext, notice the "T" declaration to pass the type (and return a "T" typed object)
_T = TypeVar("_T") class ApplicationContext(Context): ... def resolve( self, t: Type[_T] ) -> _T: return self.container.resolve(t)
Because you can create -not singleton- components on the fly directly on the context and i preferred an uniform approach
singleton: CompType = ctx.resolve(CompType) new_instance: CompType = ctx.autowire(CoompType) <pre> And this call is now part of the context! ### Fun with DI Now you can instantiate with a call to resolve the entry point for your system, for example running a web server (just an idea [here](https://pythonbasics.org/webserver/) ) But what exactly you can do with this approach? Declare a dependency based on ABC interface separating the implementation from the contract and avoiding polluting with not needed import. <pre> class Interface(ABC): .. ---------------------------------- import interface @component class Implementation(Interface): ... ---------------------------------- import interface @component class ActorClass: def __init__(self, depndency: Interface):
Define an ABC interface and retrieve all classes implementing the "interface" as dependency
@component class Impl1(Interface): ... ---------------------------------- @component class Impl2(Interface): ... ---------------------------------- import interface @component class MixerClass: def __init__(self, depndencies: list[Interface]):
I can then add simply any kind of plugins, just to give an idea You can check on wikipedia to have a small idea about what this is ;)
Python does not have a strong concept for "annotations", tags added to functions and classes to add metadata to the definition. Python has instead decorators, similar to the concept of AOP in another language, the functions are wrapped (when loading the modules) with other functionalities like logging or other. Let see an example:
def a_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): """A wrapper function""" # Extend some capabilities of func return func(*args, **kwargs) return wrapper @a_decorator def myfunction(something):
This is a decorator, it wraps myfunction and execute what is in the wrapper function, then returning the original function result.
These are loaded during the loading of the module (and this will be important)
Bottle uses decorators to register the routes, but its decorators works only when added to "global" functions and not bound to classes. I needed a way to load the decorators, connect them to the method definition, and be able to find them when inspecting the definitions of the loaded classes.
I defined my decorators
def qroute(path, verb="GET"): def decorator(func): if not hasattr(func, "_decorators"): func._decorators = [] func._decorators.append({'type': 'qroute', 'path': path, 'verb': verb}) @functools.wraps(func) def wrapper(*a, **ka): return func(*a, **ka) return wrapper return decorator
Just notice a few points
At this point i need a way to detect the decorators on a class definition
def find_decorators(clazz): methodNames = dir(clazz) result = {} for methodName in methodNames: if methodName.startswith("_"): continue method = getattr(clazz, methodName) if not hasattr(method, "_decorators"): continue result[methodName + str(signature(method))] = {'decorators': method._decorators, 'method': method, 'name': methodName} return result
The only little problem is that it can't handle overrided methods. But essentially it loads the method names, seek the attribute on the type definition a map with the method name, the method (definition) and the list of decorators
I can now use them, here i have a list of Controller i search of each the decorators.
def __init__(self, controllers: list[Controller], authProvider: AuthProvider): self.controllers = controllers for controller in self.controllers: ... t = type(controller) decorators = find_decorators(t) for key, value in decorators.items(): instance_method = getattr(controller, value["name"]) decorators = value["decorators"] routes = [p for p in decorators if p["type"] == "qroute"] if len(routes)==1: route = routes[0] bottle.route(route["path"], callback=instance_method, method=route["verb"])
Basically
This is possible only because of DI that has the responsibility to find all the relevant classes. One other approach could be scanning manually all relevant modules and scan the world
Everything is now in place for an OOP Bottle
To run a bottle application should simply start like this (use an empty string instead of localhost to listen an all addresses from bottle import route
@route('/hello/<name>') def index(name): return bottle.template('<b>Hello {{name}}</b>!', name=name) bottle.run(host='localhost', port=8080)
But now i created the "qroute" annotation and i can create a fantastic bottle service!
@component class BottleService: def __init__(self, controllers: list[Controller], authProvider: AuthProvider): self.controllers = controllers for controller in self.controllers: ... t = type(controller) decorators = find_decorators(t) for key, value in decorators.items(): instance_method = getattr(controller, value["name"]) decorators = value["decorators"] route = [p for p in decorators if p["type"] == "qroute"] auth = [p for p in decorators if p["type"] == "qauth"] if len(route) == 1: route = route[0] bottle.route(route["path"], callback=instance_method, method=route["verb"]) def run(self, app=None, server='wsgiref', host='127.0.0.1', port=8080, interval=1, reloader=False, quiet=False, plugins=None, debug=None, **kargs): bottle.run(app, server, host, port, interval, reloader, quiet, plugins, debug, **kargs)
Now implementing the Controller(ABC) i can easily create a class controller! Placed anywhere
@component class HelloController(Controller): @qroute("/api/hello") def hello(self): return ("Hello")
If you look into the code you can notice that leveraging this approach i added "decorators" even for basic authentication and authorization with the AuthProvider ABC!
Another intersting point is the StaticController, that adds all the route for the static files, called and initialized via the mapRoutes ovewritable method
Have fun playing with this :)