Custom managers and hooks

This document will explain how managers and hooks are used in Socon and how you can use it in your own framework. Managers and hooks are powerful features of Socon that will make your framework generic and maintainable.

Note

A tutorial is available on managers and hooks that you should do if you are new to Socon.

Manager

The term manager in Socon describes a super class that provides features to find, register and manipulate hooks. For example, the commands management module contain a CommandManager which inherit from the base class BaseManager. This class, registers every command that are in the management/commands directory alongside the configuration in which they are linked to.

By subclassing your class from the BaseManager and by explicitly placing your managers class into a file called managers.py at the root of a plugin, a project or in the common space; Socon will automatically register that class as a manager. When you create your manager, you will need to define two mandatory attributes:

  1. name: The name of the manager. This name must be unique across your entire framework including the plugins.

  2. lookup_module: The full python path to the module that contains hooks linked to this manager.

Here is quick example. Let’s create a BuildManager at the root of the common space in the manager.py like so:

myframework/
    myframework/
        __init__.py
        management/
        managers.py
    manage.py

Let’s have a closer look to the managers.py:

myframework/managers.py
from socon.core.manager import BaseManager


class BuildManager(BaseManager):
    name = 'build'
    lookup_module = 'builder'

When Socon starts, it registers all registry configurations and imports at the same time every managers.py. The import, registers every manager in the ManagerRegistry class.

Access your managers

You can access any manager by importing socon.core.manager.managers. managers is an instance of ManagerRegistry class. Here is an example to get the BuildManager manager. We will pretend to have a build command that will access this manager.

myframework/management/commands/build.py
from socon.core.management.base import ProjectCommand
from socon.core.manager import managers


class BuildCommand(ProjectCommand):
    name = 'build'

def handle(self, config, project_config):
    manager = managers.get_manager('build')

Accessing your manager, will let you find, access and use your hooks.

Module introspection

When a manager searches for a hook, it calls the get_modules() method. This method by default returns the RegistryConfig.name + the lookup_module. For example: projects.apollo.builder if we are looking for the builder module inside the project apollo. Then we import the module and every class that inherits from Hook will be registered to its manager.

By overriding the get_modules() method, you can provide your own way of finding modules. That is what we have done for the CommandManager. The method returns every modules that are in the management/commands directory.

Hooks

The term hooks in Socon describes a piece of code, generally a subclass of the base class Hook that a user can define to extend or replace the current code being executed. ProjectCommand and BaseCommand are hooks of a CommandManager. All subclass of these two super classes, will be registered in the CommandManager.

Note

Hooks are nothing less than a class that is linked to a manager to do anything you want. As we said in the beginning of this document BaseCommand and ProjectCommand inherit from Hook and define how a command works. Both of these classes are linked to the CommandManager that defines the way we find these commands and the way we print the helper when you type python manage.py help.

By subclassing a class from the Hook base class, you make it discoverable by the manager it will be linked to.

In our previous example, we have defined a BuildManager. We now need to link hooks to this class. For that we must declare a Hook subclass and make it discoverable by placing it inside a builder.py module. Previously, we have define the BuildManager with a lookup_module equal to builder. Let’s create a builder.py next to the managers.py.

myframework/builder.py
from socon.core.manager import Hook


class SpaceCraftBuilder(Hook):
    manager = 'spacecraft_manager'

    # Name of the builder
    name = "default_builder"

    def build(self, spacecraft: SpaceCraft):
        print("Building {}".format(spacecraft.name))

The important thing here is the name of the manager that this hook and all its subclass will be linked to. This is really important. If you forget it, when Socon will import the module, it will throw you an error. Every class that inherit from Hook must define the manager attribute.

Note

If you subclass SpaceCraftBuilder, you don’t need to redefine the manager as it will be inherited.

Access your hooks

In order to access your hooks, you first need to access your manager. Then you need to search for hooks and for that you have two options:

  1. find_hooks_impl(). This method will look for hooks in a specific config. This means that you will have to provide a for example a ProjectConfig, and it will search for every hook in that config.

  2. find_all(). This method will search in every config. It will search in every installed config and in the common space.

Now that you have searched for hooks, you can call the get_hook() method. This method, requires a RegistryConfig like a ProjectConfig and the name of the hook to search for. Here is a quick example:

# ..
class BuildCommand(ProjectCommand):
    name = 'build'

    def handle(self, config, project_config):
        manager = managers.get_manager('build')
        manager.find_all()
        hook = manager.search_hook_impl(project_config, 'default_builder`)
        builder = hook()
        builder.build(
            type('SpaceCraft', (object,), {'name': 'Saturn Ib'})()
        )

As seen above, we are retrieving the build manager that we have defined in our previous example. We request the manager to find_all() hooks in every config. Afterwards we get the builder using the search_hook_impl() method and we initialize our builder. Finally we build our spacecraft.

Note

We could have used the get_hook() method as we know that there is a builder for that project. Be aware that if the hook is not found using that method, it will return None.

Overriding hooks

During the search (when we call the find_all() method), each hook is registered in a manager, like the BuildManager, and saved alongside its config object. When Socon looks for a hook using search_hook_impl() it will proceed as follows:

  1. Did the user pass a config object?

    Yes, search the hook for that config. It is found return it else continue.

  2. Search in the common space config for hooks.

    If it’s found return it else continue.

  3. Search in the plugins.

    If it’s found return it else continue.

  4. Search in built-in Socon hooks

    if it’s found return it else continue.

  5. Raise HookNotFound.

To override a hook, the new hook must have the same name as the hook you want to override. They also need to be part of the same manager. Let’s take an example to illustrate what we just said. Above, we have created a SpaceCraftBuilder. Let’s say that a project wants to implement its own builder with a specific feature. We would create a new SpaceCraftBuilder in the project itself. This way, our builder is found first when we are looking for it.

projects/apollo/builder.py
from myframework.builder import SpaceCraftBuilder

class ProjectSpaceCraftBuilder(SpaceCraftBuilder):
    name = 'default_builder`

    def build(self, spacecraft: SpaceCraft):
        self.prepare_docs()
        super().build(spacecraft)

    def prepare_docs(self):
        print("Prepare documents before building the spacecraft")

As you can see, the builder has the same name as the one we declared earlier. It also inherit from the one in the common space. This builder will do the exact same thing as the previous one but it will add a new method to prepare the documentation before building the spacecraft. Now when the manager will look for the default_builder hook, it will return this one first.

Abstract hooks

The term abstract means that the hook you will define will not be registered and available in the manager. It is useful when you want to make a hook as an interface for other hooks.

In our above example, we could have changed the SpaceCraftBuilder as a BaseBuilder class defined as abstract. And then inherit from that class to create the SpaceCraftBuilder.

myframework/builder.py
from socon.core.manager import Hook


class BaseBuilder(Hook, abstract=True):
    manager = 'spacecraft_manager'

    # Name of the builder
    name = "default_builder"

    def build(self, spacecraft: SpaceCraft):
        raise NotImplementedError(...)

class SpaceCraftBuilder(Hook):

    def build(self, spacecraft: SpaceCraft):
        print("Building {}".format(spacecraft.name))