Captain Hook

August 15, 2018 by Florian Einfalt

Pondering over the Shotgun hook framework at work today, I wrote a simple hook decorator handling pre-function execution and post-function execution events.

A simple version of a hook decorator that would invoke one or more callbacks after the main function has executed could look something like this:

class Hook(object):

    def __init__(self, func):
        self.callbacks = []
        self.basefunc = func

    def callback(self, func):
        if callable(func):
            try:
                self.callbacks.remove(func)
            except ValueError:
                pass
            self.callbacks.append(func)
        return func

    def __call__(self, *args, **kwargs):
        result = self.basefunc(*args, **kwargs)
        for func in self.callbacks:
            newresult = func(result)
            result = result if newresult is None else newresult
        return result

Usage of this simple model would be pretty straight forward, like so:

# Declaring the main function as a hook
@Hook
def main_func(num):
    return int(num)

@main_func.callback
def argument(num):
    print('argument: {}'.format(num))

@main_func.callback
def power(num):
    return num ** 2

print(main_func(4))

# argument: 4
# 16

Now to the second version of the hook decorator, supporting pre and post execution callbacks.

class Hook(object):

    def __init__(self, func):
        self.pre_callbacks = []
        self.post_callbacks = []
        self.basefunc = func

    def pre(self, func):
        if callable(func):
            try:
                self.pre_callbacks.remove(func)
            except ValueError:
                pass
            self.pre_callbacks.append(func)
        return func

    def post(self, func):
        if callable(func):
            try:
                self.post_callbacks.remove(func)
            except ValueError:
                pass
            self.post_callbacks.append(func)
        return func

    def __call__(self, *args, **kwargs):
        for func in self.pre_callbacks:
            args, kwargs = func(*args, **kwargs)
        result = self.basefunc(*args, **kwargs)
        for func in self.post_callbacks:
            newresult = func(result)
            result = result if newresult is None else newresult
        return result

The hook decorator takes an arbitrary amount of pre-execution callback(s) as well as post-execution callback(s), which are themselves declared using the decorator syntax. The hook will pass all the *args and **kwargs that it will pass to the main function to the pre callback(s).

The result of the main function will be passed to the post callback(s).

The fact that we can still use this hook as a Python decorator makes usage extremely simple:

# Declaring the main function as a hook
@Hook
def main_func(num):
    return int(num)

# Declaring a `pre` function
@main_func.pre
def notify(*args, **kwargs):
    print("args: {0}".format(args))
    print("kwargs: {0}".format(kwargs))
    return args, kwargs

# Declaring a `post` function
@main_func.post
def square(result):
    return result ** 2


# Running the main function with the hooks
print(main_func(4))

# args: (4,)
# kwargs: {}
# 16

© 2018-2020 Florian Einfalt