Main Image

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

Setup Dependency Injection

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 ;)

Annot..Decorators

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

Bottle OOP

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")

Left to the reader

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 :)


Last modified on: September 20, 2024