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.