Usage

The encore.events package provides a fairly straightforward event dispatcher.

Event Classes

Basic filtering is based upon the class of the event, so most users will want to define their own set of event classes, but a number of standard BaseEvent subclasses are provided by the module. An event class can be as simple as:

from encore.events.api import BaseEvent

class SaveEvent(BaseEvent):
    """ Event generated when a file is saved

    Attributes
    ----------
    directory : path
        The directory that the file that was saved in.
    filename : string
        The name of the file that was saved.

    """

The BaseEvent‘s __init__() method takes any additional keyword arguments it is supplied, and adds them as attributes on the object. Every event has a source object which is the object which generated the event. You can create an instance of an event like so:

event = SaveEvent(source=obj, directory='foo/bar', filename='baz.txt')

Because filtering of events respects the class heirarchy of events, you will frequently want to define some abstract base classes to assist with filtering:

from encore.events.api import BaseEvent

class FileEvent(BaseEvent):
    """ Event generated when a file is operated upon

    Attributes
    ----------
    directory : path
        The directory that the file that was saved in.
    filename : string
        The name of the file that was saved.

    """

class OpenEvent(FileEvent):
    pass

class SaveEvent(FileEvent):
    pass

class DeleteEvent(FileEvent):
    pass

In the above example, you will probably never generate an instance of FileEvent, but you may set up listeners for such events.

Event Managers

To emit events, you will then need to ensure that your application has a (usually unique) event manager to handle the dispatch of events. Creating an event manager is straightforward:

from encore.events.api import EventManager

event_manager = EventManager()

More typically you will have some sort of global application state object that is responsible for managing things like event managers, and then you might use it as follows:

import os
from uuid import uuid4
from encore.events.api import EventManager, ProgressManager
from .app_events import SaveEvent

class App(object):
    def __init__(self):
        self.event_manager = EventManager()

class File(object):
    def __init__(self, app, directory, filename, data=''):
        self.app = app
        self.directory = directory
        self.filename = filename
        self.data = data

    def save(self):
        event_manager = self.app.event_manager
        path = os.path.join(self.directory, self.filename)
        op_id = uuid4()
        try:
            with open(path, 'wb') as fp:
                steps = range(0, len(data), 2**20)
                with ProgressManager(event_manager, self, op_id,
                        'Saving "%s"' % path, len(steps)) as progress:
                    for i, pos in enumerate(step):
                        fp.write(self.data[i:i+2**20])
                        progress('Saving "%s" (%d of %d bytes)' % (path, pos, len(data)),
                            step=i+1)
        else:
            event_manager.emit(SaveEvent(source=self, directory=self.directory,
                filename=self.filename))

Notice the use of the standard ProgressManager subclasses to generate progress update events while writing out the data.

Listeners

A listener is simply a function which expects to be given an event instance and does something with it. For example, we could write a listener which listens for SaveEvents and logs them to a logger:

import logging
import os

logger = logging.getLogger(__name__)

def save_logger(event):
    path = os.path.join(event.directory, event.filename)
    logger.info("Saved file '%s'" % path)

Once you have a listener, it can be connected to listen for particular classes of events via the event manager:

event_manager.connect(SaveEvent, save_logger)

Once the listener is connected, the save_logger() function will be called every time that a SaveEvent is emitted. A listener can be explicitly disconnected by calling the disconnect() method of the event manager:

event_manager.disconnect(SaveEvent, save_logger)

A listener which is a bound method will be disconnected automatically if the underlying instance has been garbage-collected, so in many instances you will not need to worry about explicitly disconnecting listeners.

In the above example, you would be more likely to want to log all FileEvents rather than save events. This could be achieved by something like:

def file_event_logger(event):
    path = os.path.join(event.directory, event.filename)
    logger.info("%s: file '%s'" % (event.__class__.__name__, path))

event_manager.connect(FileEvent, file_event_logger)

This will call the file_event_logger() function every time that a subclass of FileEvent is emitted.

Listener Priority

It is possible to have multiple listeners on a particular class, and you may want some listeners to run before other listeners. In particular, a listener may mark an event as “handled” in which case processing stops and all lower priority listeners do not get to see the event.

For instance, in the above example, we might want to have both the save_logger() and file_event_logger() active. In that case we don’t want to have save events logged twice, so we can do the following:

def save_logger(event):
    path = os.path.join(event.directory, event.filename)
    logger.info("Saved file '%s'" % path)
    event.mark_as_handled()

event_manager.connect(SaveEvent, save_logger, priority=100)
event_manager.connect(FileEvent, file_event_logger, piority=50)

By setting the priority of save_logger() higher than that of file_event_logger(), it will get called first, and when it calls the event’s mark_as_handled() method then it will prevent any lower-priority events from firing.

In the default event manager implementation, listeners of the same priority are called in the order in which they were connected.

Filtering

On occassion a listener may only care about events from certain sources or matching certain attributes. The event manager allows a filter to be specified when connecting a listener, so that the listener will only be called when the filter is matched.

A filter is simply specified as a dictionary of event attribute, value pairs:

class Project(object):
    def __init__(self, app, directory):
        self.app = app
        self.directory = directory
        self._needs_compile = False
        self._connect_listener()

    def directory_listener(self, event):
        self._needs_complie = True

    def _connect_listener(self):
        self.app.event_manager.connect(SaveEvent, self.directory_listener,
            filter={'directory': self.directory})

In this example, a Project instance will have its directory_watcher() method called whenever a file is saved in the directory specified by its directory attribute.

Example: Progress Bar

As an example which ties together the concepts which have been shown so far, we will write some code which displays progress indications to standard out that look something like the following:

Saving "foo/bar/baz.txt":
[*************************************

We start with a class which is responsible for listening for the start of a progress event. For simplicty we will assume that there will only be one progress sequence happening at any given time, so we will have the class instance hook up a listener for ProgressStartEvents:

class ProgressDisplay(object):
    def __init__(self, event_manager):
        self.event_manager = event_manager
        self.event_manager.connect(ProgressStartEvent, self.start_listener)

When a ProgressStartEvent occurs, then we will print out the initial text, and set up a listener for the ProgressStepEvent and ProgressEndEvent event types:

def start_listener(self, event):
    # display initial text
    sys.stdout.write(event.message)
    sys.stdout.write(':\n[')
    sys.stdout.flush()

    # create a ProgressWriter instance
    writer = ProgressWriter(self, event.operation_id, event.steps)
    self.writers[event.operation_id] = writer

    # connect listeners
    self.event_manager.connect(ProgressStepEvent, writer.step_listener,
        filter={'operation_id': event.operation_id})
    self.event_manager.connect(ProgressEndEvent, writer.end_listener,
        filter={'operation_id': event.operation_id})

The writer class handles listening for step and end events. The end event listener simply removes the writer object from the display, which will cause it to eventually be garbage-collected and the listeners disconnected automatically:

class ProgressWriter(object):
    def __init__(self, display, operation_id, steps):
        self.display = display
        self.operation_id = operation_id
        self.steps = steps
        self._count = 0
        self._max = 75

    def step_listener(self, event):
        stars = int(round(float(event.step)/self.steps*self._max))
        if stars > self._count:
            sys.stdout.write('*'*(stars-self._count))
            sys.stdout.flush()
            self._count = stars

    def end_listener(self, event):
        if event.exit_state == 'normal':
            sys.stdout.write(']\n')
            sys.stdout.flush()
        else:
            sys.stdout.write('\n')
            sys.stdout.write(event.exit_state.upper())
            sys.stdout.write(': ')
            sys.stdout.write(event.message)
            sys.stdout.write('\n')
            sys.stdout.flush()
        del self.display[self.operation_id]

Advanced Features

Disabling Events

The event manager has methods that allow code to temporarily disable events of a certain class. These are accessed via the disable(), enable(), and is_enabled() methods. Disabling an event class will also disable any of its subclasses, so:

event_manager.disable(BaseEvent)

will disable all events.

Enabled/disabled state is kept track of on a per-class basis, so after:

event_manager.disable(SaveEvent)
event_manager.disable(FileEvent)
event_manager.enable(FileEvent)

the SaveEvent events will still be disabled.

Pre- and Post-Emit Callbacks

The event classes also have two hooks pre_emit() and post_emit() which get called immediately before and immediately after dispatch to listeners. This potentially allows Event code to perform actions based upon interactions with listeners, such as having a post_emit() method which does something sensible if an event is not handled. These hooks may also be of use for instrumenting and debugging code.

Threading

By default events are processed on the thread that they were emitted on, and the connect(), disconnect() and emit() methods should be thread-safe. Processing an event blocks that thread from further work until all listeners have been called.

The emit() method has an optional argument block which if False will cause the emit method to create a worker thread to perform the listener dispatch, and will return that thread from the function call.