Handling Events¶
This document provides information on how to handle the lifycycle events defined and sent by this package.
Background information on handling events is found in
zope.event's documentation
.
Class Based Handling¶
zope.event
includes a simple framework for dispatching
events based on the class of the event. This could be used to provide
handlers for each of the event classes defined by this package
(ObjectCreatedEvent
, etc). However, it doesn’t allow
configuring handlers based on the kind of object the event contains.
To do that, we need another level of dispatching.
Fortunately, that level of dispatching already exists within
zope.component
.
Component Based Handling¶
zope.component
includes an event dispatching framework that
lets us dispatch events based not just on the kind of the event, but
also on the kind of object the event contains.
All of the events defined by this package are implementations of
zope.interface.interfaces.IObjectEvent
. zope.component
includes special support for these kinds of events. That document
walks through a generic example in Python code. Here we will show an
example specific to life cycle events using the type of configuration
that is more likely to be used in a real application.
For this to work, it’s important that zope.component
is configured
correctly. Usually this is done with ZCML executed at startup time (we
will be using strings in this documentation, but usually this resides
in files, most often named configure.zcml
):
>>> from zope.configuration import xmlconfig
>>> _ = xmlconfig.string("""
... <configure xmlns="http://namespaces.zope.org/zope">
... <include package="zope.component" />
... </configure>
... """)
First we will define an object we’re interested in getting events for:
>>> from zope.interface import Interface, Attribute, implementer
>>> class IFile(Interface):
... data = Attribute("The data of the file.")
... name = Attribute("The name of the file.")
>>> @implementer(IFile)
... class File(object):
... data = ''
... name = ''
Next, we will write our subscriber. Normally, zope.event
subscribers take just one argument, the event object. But when we use
the automatic dispatching that zope.component
provides, our
function will receive two arguments: the object of the event, and
the event. We can use the decorators that zope.component
supplies
to annotate the function with the kinds of arguments it wants to
handle. Alternatively, we could specify that information when we
register the handler with zope.component (we’ll see an example of that
later).
>>> from zope.component import adapter
>>> from zope.lifecycleevent import IObjectCreatedEvent
>>> @adapter(IFile, IObjectCreatedEvent)
... def on_file_created(file, event):
... print("A file of type '%s' was created" % (file.__class__.__name__))
Finally, we will register our handler with zope.component. This is also usually done with ZCML executed at startup time:
>>> _ = xmlconfig.string("""
... <configure xmlns="http://namespaces.zope.org/zope">
... <include package="zope.component" file="meta.zcml" />
... <subscriber handler="__main__.on_file_created"/>
... </configure>
... """)
Now we can send an event noting that a file was created, and our handler will be called:
>>> from zope.lifecycleevent import created
>>> file = File()
>>> created(file)
A file of type 'File' was created
Other types of objects don’t trigger our handler:
>>> created(object)
The hierarchy is respected, so if we define a subclass of File
and
indeed, even a sub-interface of IFile
, our handler will be
invoked.
>>> class SubFile(File): pass
>>> created(SubFile())
A file of type 'SubFile' was created
>>> class ISubFile(IFile): pass
>>> @implementer(ISubFile)
... class IndependentSubFile(object):
... data = name = ''
>>> created(IndependentSubFile())
A file of type 'IndependentSubFile' was created
We can further register a handler just for the subinterface we created. Here we’ll also demonstrate supplying this information in ZCML.
>>> def generic_object_event(obj, event):
... print("Got '%s' for an object of type '%s'" % (event.__class__.__name__, obj.__class__.__name__))
>>> _ = xmlconfig.string("""
... <configure xmlns="http://namespaces.zope.org/zope">
... <include package="zope.component" file="meta.zcml" />
... <subscriber handler="__main__.generic_object_event"
... for="__main__.ISubFile zope.lifecycleevent.IObjectCreatedEvent" />
... </configure>
... """)
Now both handlers will be called for implementations of ISubFile
,
but still only the original implementation will be called for base IFiles
.
>>> created(IndependentSubFile())
A file of type 'IndependentSubFile' was created
Got 'ObjectCreatedEvent' for an object of type 'IndependentSubFile'
>>> created(File())
A file of type 'File' was created
Projects That Rely on Dispatched Events¶
Handlers for life cycle events are commonly registered with
zope.component
as a means for keeping projects uncoupled. This
section provides a partial list of such projects for reference.
As mentioned in Quick Start, the containers provided by zope.container generally automatically send the correct life cycle events.
At a low-level, there are utilities that assign integer IDs to objects
as they are created such as zope.intid and zc.intid.
zc.intid
, in particular, documents the way it uses events.
zope.catalog
can automatically index documents as part of
handling life cycle events.
Containers and Sublocations¶
The events ObjectAddedEvent
and ObjectRemovedEvent
usually need to be (eventually) sent in pairs for any given object.
That is, when an added event is sent for an object, for symmetry
eventually a removed event should be sent too. This makes sure that
proper cleanup can happen.
Sometimes one object can be said to contain other objects. This is obvious in the case of lists, dictionaries and the container objects provided by zope.container, but the same can sometimes be said for other types of objects too that reference objects in their own attributes.
What happens when a life cycle event for such an object is sent? By default, nothing. This may leave the system in an inconsistent state.
For example, lets create a container and add some objects to it. First we’ll set up a generic event handler so we can see the events that go out.
>>> _ = xmlconfig.string("""
... <configure xmlns="http://namespaces.zope.org/zope">
... <include package="zope.component" file="meta.zcml" />
... <subscriber handler="__main__.generic_object_event"
... for="* zope.interface.interfaces.IObjectEvent" />
... </configure>
... """)
Got...
>>> from zope.lifecycleevent import added
>>> container = {}
>>> created(container)
Got 'ObjectCreatedEvent' for an object of type 'dict'
>>> object1 = object()
>>> container['object1'] = object1
>>> added(object1, container, 'object1')
Got 'ObjectAddedEvent' for an object of type 'object'
We can see that we got an “added” event for the object we stored in the container. What happens when we remove the container?
>>> from zope.lifecycleevent import removed
>>> tmp = container
>>> del container
>>> removed(tmp, '', '')
Got 'ObjectRemovedEvent' for an object of type 'dict'
>>> del tmp
We only got an event for the container, not the objects it contained!
If the handlers that fired when we added “object1” had done anything
that needed to be undone for symmetry when “object1” was removed
(e.g., if it had been indexed and needed to be unindexed) the system
is now corrupt because those handlers never got the
ObjectRemovedEvent
for “object1”.
The solution to this problem comes from zope.container. It defines
the concept of ISubLocations
: a
way for any given object to inform other objects about the objects it
contains (and it provides a default implementation of
ISubLocations
for
containers). It also provides a function
that will send
events that happen to the parent object for all the child objects
it contains.
In this way, its possible for any arbitrary life cycle event to
automatically be propagated to its children without any specific
caller of remove
, say, needing to have any specific knowledge
about containment relationships.
For this to work, two things must be done:
- Configure zope.container. This too is usually done in ZCML with
<include package="zope.container"/>
. - Provide an adapter to
ISubLocations
when some object can contain other objects that need events.