Connect Python objects to blinker signals

mblayman

Matt Layman

Posted on October 23, 2019

Connect Python objects to blinker signals

I started using blinker for handroll. Blinker is a signal generation library for broadcasting events. The library lets signalers send messages to connected receiver functions. I will explain how I convinced Blinker to talk to objects instead of pure Python functions.

The example code is going to handle a "frobnicated" signal. Remember, the signal itself is not very important.

import blinker

frobnicated = blinker.signal('frobnicated')

frobnicated is a named signal. In a real project, you might put all your signals in a single module. Pelican does this nicely. Grouping all your signals in one place gives signal consumers a clear view of what is available.

class Receiver(object):

    def __init__(self):
        def handle_frobnicated(sender, **kwargs):
            self.on_frobnicated(sender, **kwargs)
        self.handle_frobnicated = handle_frobnicated
        frobnicated.connect(handle_frobnicated)

    def on_frobnicated(self, sender, **kwargs):
        print(sender, kwargs['message'])

The __init__ method is where all the magic happens. The first thing to notice is the use of an inner function, handle_frobnicated. The inner function uses the method signature that the signal will invoke, and delegates to Receiver.on_frobnicated. Why? This is necessary because Blinker can't pass self to receiver functions. handle_frobnicated acts as a closure on self which lets the signal call the instance method.

        self.handle_frobnicated = handle_frobnicated

That seems like a strange line, doesn't it? Blinker does some funny stuff with references. Without storing the inner function, Blinker will delete a weak function reference and the inner function will no longer be among the signal's receivers. I stared at the Blinker source code for a long time to figure that mystery out.

The last line in __init__ connects the signal to the inner function. The receiver is ready to handle frobnicated events.

if __name__ == '__main__':
    receiver = Receiver()
    for i in range(10):
        frobnicated.send('Sender %s' % i, message='hello')

The code to fire the signal is fairly boring. Notice that frobnicated.send has no need for receiver. The publisher is disconnected from subscriber at this stage. The final result looks like:

$ python blink_object.py
Sender 0 hello
Sender 1 hello
Sender 2 hello
Sender 3 hello
Sender 4 hello
Sender 5 hello
Sender 6 hello
Sender 7 hello
Sender 8 hello
Sender 9 hello

By connecting a signal to an object, you get all the benefits that come along with classes. Rather than making a monsterous function, you could use various instance methods within the handler. This flexibility is a boon for unit testing. The gain has similar advantages to using class based views in Django rather than function views.

Here's the full example.

import blinker

frobnicated = blinker.signal('frobnicated')


class Receiver(object):

    def __init__(self):
        def handle_frobnicated(sender, **kwargs):
            self.on_frobnicated(sender, **kwargs)
        self.handle_frobnicated = handle_frobnicated
        frobnicated.connect(handle_frobnicated)

    def on_frobnicated(self, sender, **kwargs):
        print(sender, kwargs['message'])


if __name__ == '__main__':
    receiver = Receiver()
    for i in range(10):
        frobnicated.send('Sender %s' % i, message='hello')

This article first appeared on mattlayman.com.

đź’– đź’Ş đź™… đźš©
mblayman
Matt Layman

Posted on October 23, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related