« CPS Second Bug Day Wrapup | Main | Object event dispatching in Zope »

08 November 2005

Events in Zope 2.9

<p>Zope 2.9 (and Zope 2.8 when using Five 1.2) introduces a big change: Zope 3 style container events.</p> <p>With container events, you finally have the ability to react to things happening to objects without have to subclass <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt>, <tt class="docutils literal"><span class="pre">manage_beforeDelete</span></tt> or <tt class="docutils literal"><span class="pre">manage_afterClone</span></tt>. Instead, you just have to register a subscriber for the appropriate event, for instance IObjectAddedEvent, and make it do the work.</p> <p>Indeed, the old methods like <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt> are now deprecated, you shouldn't use them anymore.</p> <p>Let's see how to migrate your products.</p> <p class="section " id="old-product"> <h1><a name="old-product">Old product</a></h1> <p>Suppose that in an old product you have code that needs to register through a central tool whenever a document is created. Or it could be indexing itself. Or it could initialize an attribute according to its current path. Code like:</p> <pre class="literal-block">class CoolDocument(...): ... def manage_afterAdd(self, item, container): self.mangled_path = mangle('/'.join(self.getPhysicalPath())) getToolByName(self, 'portal_cool').registerCool(self) super(CoolDocument, self).manage_afterAdd(item, container) def manage_afterClone(self, item): self.mangled_path = mangle('/'.join(self.getPhysicalPath())) getToolByName(self, 'portal_cool').registerCool(self) super(CoolDocument, self).manage_afterClone(item) def manage_beforeDelete(self, item, container): super(CoolDocument, self).manage_beforeDelete(item, container) getToolByName(self, 'portal_cool').unregisterCool(self) </pre> <p>This would be the best practice in Zope 2.8. Note the use of <tt class="docutils literal"><span class="pre">super()</span></tt> to call the base class, which is often omitted because people "know" that SimpleItem for instance doesn't do anything in these methods.</p> <p>If you run this code in Zope 2.9, you will get deprecation warnings, telling you that:</p> <pre class="literal-block">Calling Products.CoolProduct.CoolDocument.CoolDocument.manage_afterAdd is deprecated when using Five, instead use event subscribers or mark the class with <five:deprecatedManageAddDelete/> </pre> </p> <p class="section " id="using-five-deprecatedmanageadddelete"> <h1><a name="using-five-deprecatedmanageadddelete">Using five:deprecatedManageAddDelete</a></h1> <p>The simplest thing you can do to deal with the deprecation warnings, and have correct behavior, is to add in your products a <tt class="docutils literal"><span class="pre">configure.zcml</span></tt> file containing:</p> <pre class="literal-block"><configure xmlns="http://namespaces.zope.org/zope" xmlns:five="http://namespaces.zope.org/five"> <five:deprecatedManageAddDelete class="Products.CoolProduct.CoolDocument.CoolDocument"/> </configure> </pre> <p>This tells Zope that you acknowledge that your class contains deprecated methods, and ask it to still call them in the proper manner. So Zope will be sending events when an object is added, for instance, and in addition call your old <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt> method.</p> <p>One subtlety here is that you may have to modify you methods to just do their work, and not call their super class. This is necessary because proper events are already dispatched to all relevant classes, and the work of the super class will be done trough events, you must not redo it by hand. If you call the super class, you will get a warning, saying for instance:</p> <pre class="literal-block">CoolDocument.manage_afterAdd is deprecated and will be removed in Zope 2.11, you should use an IObjectAddedEvent subscriber instead. </pre> <p>The fact that you must "just do your work" is especially important for the rare cases where people subclass the <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt> of object managers like folders, and decided to reimplement recursion into the children themselves. If you do that, then there will be two recursions going on in parallel, the one done by events, and the one done by your code. This would be bad.</p> </p> <p class="section " id="using-subscribers"> <h1><a name="using-subscribers">Using subscribers</a></h1> <p>In the long run, and before Zope 2.11 where <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt> and friends will be removed, you will want to use proper subscribers.</p> <p>First, you'll have to write a subscriber that "does the work", for instance:</p> <pre class="literal-block">def addedCoolDocument(ob, event): """A Cool Document was added to a container.""" self.mangled_path = mangle('/'.join(self.getPhysicalPath())) </pre> <p>Note that we're not calling the <tt class="docutils literal"><span class="pre">portal_cool</span></tt> tool anymore, because presumably this tool will also be modified to do its work through events, and will have a similar subscriber doing the necessary <tt class="docutils literal"><span class="pre">registerCool</span></tt>. Note also that here we don't care about the event, but in more complex cases we would.</p> <p>Now we have to register our subscriber for our object. To do that, we need to "mark" our object through an interface. We can define in our product's <tt class="docutils literal"><span class="pre">interfaces.py</span></tt>:</p> <pre class="literal-block">from zope.interface import Interface, Attribute class ICoolDocument(Interface): """Cool Document.""" mangled_path = Attribute("Our mangled path.") ... </pre> <p>Then the class CoolDocument is marked with this interface:</p> <pre class="literal-block">from zope.interface import implements from Products.CoolProduct.interfaces import ICoolDocument class CoolDocument(...): implements(ICoolDocument) ... </pre> <p>Finally we must link the event and the interface to the subscriber using zcml, so in <tt class="docutils literal"><span class="pre">configure.zcml</span></tt> we'll add:</p> <pre class="literal-block">... <subscriber for="Products.CoolProduct.interfaces.ICoolDocument zope.app.container.interfaces.IObjectAddedEvent" handler="Products.CoolProduct.CoolDocument.addedCoolDocument" /> ... </pre> <p>And that's it, everything is plugged. Note that IObjectAddedEvent takes care of both <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt> and <tt class="docutils literal"><span class="pre">manage_afterClone</span></tt>, as it's sent whenever a new object is placed into a container. However this won't take care of moves and renamings, we'll see below how to do that.</p> </p> <p class="section " id="event-dispatching"> <h1><a name="event-dispatching">Event dispatching</a></h1> <p>When an IObjectEvent (from which all the events we're talking here derive) is initially sent, it concerns one object. For instance, a specific object is removed. The <tt class="docutils literal"><span class="pre">event.object</span></tt> attribute is this object.</p> <p>To be able to know about removals, we could just subscribe to the appropriate event using a standard event subscriber. In that case, we'd have to filter "by hand" to check if the object removed is of the type we're interested in, which would be a chore. In addition, any subobjects of the removed object wouldn't know what happens to them, and for instance they wouldn't have any way of doing some cleanup before they disappear.</p> <p>To solve these two problems, Zope 3 has an additional mechanism by which any IObjectEvent is redispatched using multi-adapters of the form <tt class="docutils literal"><span class="pre">(ob,</span> <span class="pre">event)</span></tt>, so that a subscriber can be specific about the type of object it's interested in. Furthermore, this is done recursively for all sublocations <tt class="docutils literal"><span class="pre">ob</span></tt> of the initial object. The <tt class="docutils literal"><span class="pre">event</span></tt> won't change though, and <tt class="docutils literal"><span class="pre">event.object</span></tt> will still be the original object for which the event was initially sent (this corresponds to <tt class="docutils literal"><span class="pre">self</span></tt> and <tt class="docutils literal"><span class="pre">item</span></tt> in the <tt class="docutils literal"><span class="pre">manage_afterAdd</span></tt> method -- <tt class="docutils literal"><span class="pre">self</span></tt> is <tt class="docutils literal"><span class="pre">ob</span></tt>, and <tt class="docutils literal"><span class="pre">item</span></tt> is <tt class="docutils literal"><span class="pre">event.object</span></tt>).</p> <p>Understanding the hierarchy of events is important to see how to subscribe to them.</p> <blockquote> <ul class="simple"> <li>IObjectEvent is the most general. Any event focused on an object derives from this.</li> <li>IObjectMovedEvent is sent when an object changes location or is renamed. It is quite general, as it also encompasses the case where there's no old location (addition) or no new location (removal).</li> <li>IObjectAddedEvent and IObjectRemovedEvent both derive from IObjectMovedEvent.</li> <li>IObjectCopiedEvent is sent just after an object copy is made, but this doesn't mean the object has been put into its new container yet, so it doesn't have a location.</li> </ul> </blockquote> <p>There are only a few basic use cases about what one wants to do with respect to events (but you might want to read the full story in Five/tests/event.txt).</p> <p>The first use case is the one where the object has to be aware of its path, like in the CoolDocument example above. That's strictly a Zope 2 concern, as Zope 3 has others ways to deal with this.</p> <p>In Zope 2 an object has a new path through creation, copy or move (rename is a kind of move). The events sent during these three operations are varied: creation sends IObjectAddedEvent, copy sends IObjectCopiedEvent then IObjectAddedEvent, and move sends IObjectMovedEvent.</p> <p>So to react to new paths, we have to subscribe to IObjectMovedEvent, but this will also get us any IObjectRemovedEvent, which we'll have to filter out by hand (this is unfortunate, and due to the way the Zope 3 interface hierarchy is organized). So to fix the CoolDocument configuration we have to add:</p> <pre class="literal-block">def movedCoolDocument(ob, event): """A Cool Document was moved.""" if not IObjectRemovedEvent.providedBy(event): addedCoolDocument(ob, event) </pre> <p>And replace the subscriber with:</p> <pre class="literal-block">... <subscriber for="Products.CoolProduct.interfaces.ICoolDocument zope.app.container.interfaces.IObjectMovedEvent" handler="Products.CoolProduct.CoolDocument.movedCoolDocument" /> ... </pre> <p>The second use case is when the object has to do some cleanup when it is removed from its parent. This used to be in <tt class="docutils literal"><span class="pre">manage_beforeDelete</span></tt>, now we can do the work in a <tt class="docutils literal"><span class="pre">removedCoolDocument</span></tt> method and just subscribe to IObjectRemovedEvent. But wait, this won't take into account moves... So in the same vein as above, we would have to write:</p> <pre class="literal-block">def movedCoolDocument(ob, event): """A Cool Document was moved.""" if not IObjectRemovedEvent.providedBy(event): addedCoolDocument(ob, event) if not IObjectAddedEvent.providedBy(event): removedCoolDocument(ob, event) </pre> <p>The third use case is when your object has to stay registered with some tool, for instance indexed in a catalog, or as above registered with <tt class="docutils literal"><span class="pre">portal_cool</span></tt>. Here we have to know the old object's path to unregister it, so we have to be called <em>before</em> it is removed. We'll use <tt class="docutils literal"><span class="pre">IObjectWillBe...</span></tt> events, that are sent before the actual operations take place:</p> <pre class="literal-block">from OFS.interfaces import IObjectWillBeAddedEvent def beforeMoveCoolDocument(ob, event): """A Cool Document will be moved.""" if not IObjectWillBeAddedEvent.providedBy(event): getToolByName(ob, 'portal_cool').unregisterCool(ob) def movedCoolDocument(ob, event): """A Cool Document was moved.""" if not IObjectRemovedEvent.providedBy(event): getToolByName(ob, 'portal_cool').registerCool(ob) ... </pre> <p>And use an additional subscriber:</p> <pre class="literal-block">... <subscriber for="Products.CoolProduct.interfaces.ICoolDocument OFS.interfaces.IObjectWillBeMovedEvent" handler="Products.CoolProduct.CoolDocument.beforeMoveCoolDocument" /> ... </pre> <p>This has to be done if the tool cannot react by itself to objects being added and removed, which obviously would be better as it's ultimately the tool's responsibility and not the object's.</p> <p>Note that if having tests like:</p> <pre class="literal-block">if not IObjectWillBeAddedEvent.providedBy(event): if not IObjectRemovedEvent.providedBy(event): </pre> <p>seems cumbersome (and backwards), it is also possible to check what kind of event you're dealing with using:</p> <pre class="literal-block">if event.oldParent is not None: if event.newParent is not None: </pre> <p>(However be careful, the <tt class="docutils literal"><span class="pre">oldParent</span></tt> and <tt class="docutils literal"><span class="pre">newParent</span></tt> are the old and new parents <em>of the original object</em> for which the event was sent, not of the one to which the event was redispatched using the multi-subscribers we have registered.)</p> <p>The <tt class="docutils literal"><span class="pre">IObjectWillBe...</span></tt> events are specific to Zope 2 (and imported from <tt class="docutils literal"><span class="pre">OFS.interfaces</span></tt>). Zope 3 doesn't really need them, as object identity is often enough.</p> </p>

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a010536291c30970b0120a9484824970b

Listed below are links to weblogs that reference Events in Zope 2.9:

Comments

I'm Florent Guillaume, director of R&D at Nuxeo, a leading open source software vendor, which develops a complete Enterprise Content Management (ECM) software platform to help companies better produce, process, publish, archive, expose and find their information from digital assets to transactional documents.

» Follow me @efge (twtr)

» Connect on LinkedIn

» Read my corporate bio

» Visit Nuxeo.com

Customize & Configure
Nuxeo • Studio

Nuxeo • DM
Online Trial

Nuxeo • DM
Download

Nuxeo • DAM
Download

Nuxeo Connect support