2. Tutorial

2.1. Configuration

First step in using Quack Quack is to implement your own Application, which should be inheriting from qq.Application. Bare Application almost does nothing, so it needs plugins to work. In order to do that, you need to overwrite create_plugins method and add some plugins. For the sake of tutorial, we just add one: Setting’s Plugin

from qq import Application
from qq.plugins import SettingsPlugin

class MyApplication(Application):
    def create_plugins(self):
        self.plugins(SettingsPlugin('path.to.settings'))

application = MyApplication()

Application instance should be created in some module as a global variable. This object will be used in all of the places of the application. Making many instances of the same class is possible, but it is a waste of resources, so please avoid that.

2.2. Starting

Starting is very important. This is the place, where the plugins will be initalized. For example, for Logging plugin, this will be the place, where the logging will start.

The start can be done only once, but it can be done for different processes (for example for web application and celery worker), so we can switch some configuration between application’s processes. For example, one start can be for normal run, second for the tests, with mocked database. First we need to name the starts. Those are called “startpoints” and the default name is just “default”.

application = MyApplication()

def start_for_pyramid():
    application.start('pyramid')

 def start_for_celery():
    application.start('celery')

 def start_for_tests():
     application.start('tests')

Those start points are used mainly by settings plugin, so the settings can be switchable. Example for above start points:

def default():
     return {"default": 1}

 def pyramid():
     settings = default()
     settings["pyramid"] = True
     return settings

 def celery():
     settings = default()
     settings["celery"] = 'yes'

 def tests():
     settings = default()
     settings["tests"] = True is True

2.3. Using Context

After starting the application, we can use it as a context manager.

from qq import Context

app = MyApplication()
app.start()

with Context(app) as ctx:
    print(ctx["settings"])

This part shows how the plugin works in general. Every plugin returns simple value (even if it’s a dict) in context initialization. Initialization is made only when the value is called by name (so you can call it lazy initialization).

Please, be aware, that you can nest the context managers. The context will be generated once with the first with statement and ended with the same statement ended.

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

with Context(app) as c1: # this is where context is initialized
    with Context(app) as c2:
        assert id(c1) == id(c2)
# this is where the context is ended/stopped

2.4. Using Injectors and dependency injection

The most useful feature in QuackQuack are injectors. This objects are responsible for injecting values from context into methods and functions. Injectors are passed to the function as default arguments, so if you need to inject dependecy (for example in tests), you can just pass the argument when calling. In order to initialize the injectors, you need to to decorate function with ApplicationInitializer decorator.

from qq import Application
from qq import ApplicationInitializer
from qq import SimpleInjector
from qq.plugins import SettingsPlugin


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


application = MyApplication()
app = ApplicationInitializer(application)


@app
def fun(something, settings=SimpleInjector("settings")):
    print(something, settings)


application.start()
fun("something")
from unittest.mock import MagicMock
fun("something", MagicMock())
fun("something", settings=MagicMock())

If the method is a coroutine, you don’t need to do nothing. It will work the same.

from asyncio import run

from qq import Application
from qq import ApplicationInitializer
from qq import SimpleInjector
from qq.plugins import SettingsPlugin


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


application = MyApplication()
app = ApplicationInitializer(application)


@app
async def fun(something, settings=SimpleInjector("settings")):
    print(something, settings)


application.start()
run(fun('something'))

2.5. Creating Plugins

Quack Quack is designed in a way, that the core should be minimalistic, but the plugins should be responsible for all the features (like settings plugin). So the only thing you need to do is inherit from qq.Plugin. This class should be self explantory:

from typing import Any

from qq.context import Context
from qq.types import Application
from qq.types import Plugin as PluginType
from qq.types import PluginKey


class Plugin(PluginType):
    key: PluginKey = None

    def init(self, key: PluginKey):
        """
        Initialize the plugin during creation.
        key - key which is used in the Application.plugins dict for this plugin.
        """
        self.key = key

    def start(self, application: Application) -> Any:
        """
        This method will be called at the start of the Application. It will be
        called only once and the result will be set in the Application.globals.
        """

    def enter(self, context: Context) -> Any:
        """
        This method will be called when the Application will be used as context
        manager, but only when the plugin will be called. This is the enter phase.
        Result will be set in the Context dict with the self.key as the key in
        that dict.
        """

    def exit(self, context: Context, exc_type, exc_value, traceback):
        """
        This method will be called when the Application will be used as context
        manager. This is the exit phase.
        """