3. Models details

3.1. Application

Application class is very simple. The purpose of that class is to start all the plugins. The two places where this is used are create_plugins and start.

3.1.1. Create plugins

This is where you will added your plugins. Your application class should inherite from the Application class and overwrite the create_plugins method. In order to add plugin you need to add plugin object to the self.plugins OrderedDict. Order here is important, so you can not create a simple dict. As of Python 3.6, for the CPython implementation of Python, dictionaries remember the order of items inserted, but this is implementation detail, that is why the OrderDict is used here.

Dict keys of these plugins is used later on, so it is important not t o overwrite already created plugin.

There are two types of plugins. One is the plugin that the name is harcoded, so there will be only one instance of plugin. Those plugins needs to be inserted by calling the self.plugins. Other plugins can have more instances (for example if you need to have 2 databases )

class MyApplication(Application):
    def create_plugins(self):
        self.plugins["settings"] = SettingsPlugin()

Plugins can return data in couple of places. Plugin’s key is used to gather those return values. For example, data from start method of the plugin can be found in Application.globals.

app = MyApplication()
app.start("startpoint")
print(app.globals["settings"])

More about the places where the data of the plugins is stored can be found in the Plugins section.

3.1.2. Application Start

First step of the application should be starting (initializing) the Application object by simply calling the start method.

app = MyApplication()
app.start("startpoint")

start method has only one argument: startpoint. This is a name of the “start place”, which can be used later on. For example, if you have normal startup and a test run, you can use different startpoint name. This startpoint value will be used in SettingsPlugin in order to choose proper function, so the settings for normal run and tests run will be different.

app = MyApplication()

def normal_run():
    app.start("normal_run")

def tests_run():
    app.start("tests_run")

start method can accept named arguments as well. These arguments will be stored in the Application.extra dict for plugins to use.

app = MyApplication()
app.start("normal_run", anothervalue=12)

assert app.extra["anothervalue"] == 12

3.2. Plugin

3.2.1. Starting

def start(self, application: Application) -> Any:

This is the place, when the plugins are started (initialized). If there is a need to do something only once (for example: read the settings), this is the right place for this. Plugin classes have a method start. Return object will be put into Application.globals[key].

3.2.2. Entering context

def enter(self, context: Context) -> Any:

This place will be run every time the application will be used as a context manager. If you nest the with statement, this part will be executed only once. Return of the enter method will be put into Context[key].

3.2.3. Exiting context

def exit(self, context: Context, exc_type, exc_value, traceback):

As any other context manager, Plugin’s class have also the exit method. This is used to close connections or handle exceptions. Please, remember that start is run in order of creating in create_plugins, but exit plugins is run in reversed order.

3.3. Injectors and ApplicationInitializer

This feature is designed as a dependency injection. Injector is an object that gets a context and return something. This function needs to be decorated with Injector function.

Example:

from qq import Injector

@Injector
def SimpleInjector(context: Context, key: str):
    return context[key]

In order to use the injector, it needs to be provided as a default var in a function. Also, the ApplicationInitializer needs to be used for that function. The ApplicationInitializer is responsible for “starting” the injectors.

Example:

from qq import ApplicationInitializer

@ApplicationInitializer(application)
def fun(settings = SimpleInjector("settings")):
    ...

The ApplicationInitializer decorator is used to initialize the injectors with provided application. There is no need of using Application as a context manager here, the function will be used under a with statement. For example, above code can be Implemented like this:

from qq.context import Context

def fun(settings):
    ...

with Context(application) as context:
    settings = context["settings"]
    fun(settings)

The advandtage of the injectors is that you do not need to pass the context value everywhere or use the with statement. So it mitigate the boilerplate. Also, you can pass arguments instead of default values in functions. This dependency injection is very helpful in implementation of tests.

Example:

@ApplicationInitializer(application)
def fun(settings = SimpleInjector("settings")):
    return settings

def test_flow():
    mock = MagicMock()
    assert fun(mock) == mock

The ApplicationInitializer function can overwrite the application var, so you can create a function with injectors in a library, but add the application var later.

Example:

from qq import ApplicationInitializer

@ApplicationInitializer(None)
def fun(settings = SimpleInjector("settings")):
    ...

fun2 = ApplicationInitializer(application)(fun)

The ApplicationInitializer will overwrite the application value in all injectors. If those injectors would have it’s own injectors in the arguments, those injectors will have the new application value as well.