Aspect-based events in Python

Matt Soucy ()

December 9, 2014

What are events?

The super simple explaination: "on this, do this"

Functions are added to a "callback list" that happen when an event is triggered

Event sample usage

def foo():
    print("Hello, events!")

myButton.onClick.add(foo)

Observer pattern

Events are sort of a variation of the observer pattern

Observer:

It's not about calling a function, it's about sending a message...

Observer/events about "something alerts callbacks that something changed/happened"

What are aspects?

Aspects are "cross-cutting concerns"

Things that weave their way throughout the program:

Sample aspect usage

# In a totally-made-up "AspectPY", based on AspectJ
class MyAdvice(apy.Aspect):
    my_pointcut = apy.call("itertools.*")

    @apy.before(my_pointcut, target="target")
    def my_advice(self, **env):
        print("Calling", env["target"])

itertools.chain("abc", "def")
# Prints: `Calling itertools.chain`

Why are these two in one presentation?

When refactoring code, you might accidentally stumble upon this.

Original motivation

This code has all of these together:

Wait!

"Logging" was a "cross-cutting concern"

Aspects

"Logging" was spread throughout the entire codebase

Splitting logging out

Let's not care about calling it, at first.

class LogFileAspect(HaroldAspect):
    def __init__(self, logfn):
        self.logfile = open(logfn, "a")
    def on_play(self, varID, uid, song):
        # I know...legacy reasons. I'll switch to csv module soon
        print(time.strftime('%Y/%m/%d %H:%M:%S,{0},{1},{2}'
              .format(varID, uid, song)), file=self.logfile)
    def on_terminate(self):
        self.logfile.close()

Other "cross-cutting" concerns

GPIO is a nice one

Different circumstances trigger different GPIO reactions:

class GPIOAspect(HaroldAspect):
    def __init__(self, *pins):
        self.pins = pins
        GPIO.setmode(GPIO.BOARD)
        for pin in pins:
            GPIO.setup(pin, GPIO.OUT)
            GPIO.output(pin, True)
    def setPins(self, val):
        for pin in self.pins:
            GPIO.output(pin, val)
    def on_play(self, varID, uid, song):
        self.setPins(False)
    def on_stop(self):
        self.setPins(True)

What did these have in common?

Notice the on_play functions:

class LogFileAspect(HaroldAspect):
    # ...
    def on_play(self, varID, uid, song):
        # I know...legacy reasons. I'll switch to csv module soon
        print(time.strftime('%Y/%m/%d %H:%M:%S,{0},{1},{2}'
              .format(varID, uid, song)), file=self.logfile)
# ...
class GPIOAspect(HaroldAspect):
    # ...
    def on_play(self, varID, uid, song):
        self.setPins(False)

Both happen at the "same time"!

Triggering our events

We have cross-cutting concerns located in their own aspect classes

Let's make a function to call them:

class AspectWeaver(object):
    # ...
    def trigger(self, func, *args, **kwargs):
        for a in self.aspects:
            if hasattr(a, "on_"+func):
                getattr(a, "on_"+func)(*args, **kwargs)

What happened to aspects?

The two were put together pretty seamlessly:

Unplanned design

The original code was designed as "what's architecture?"

Why does this design rock?

Code for each subsystem (GPIO, logging, etc) is in one place

Code triggering parts of each subsystem is in one place:

class Harold(AspectWeaver):
     def __call__(self):
         if not self.playing:
             # ...
             self.trigger("play", varID, uid, song)
             self.playing = True

         elif time.time() >= self.endtime:
             self.trigger("stop")
             self.playing = False

         elif time.time() >= self.fadetime:
             # Fade out the music at the end.
             self.trigger("fade")

Why does this design not work all the time?

Events vs AOP vs my design

Future work