Nuxeo

Florent's Nuxeo Blog

Events in Zope 2.9

without comments

<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>

Written by Florent Guillaume

November 8th, 2005 at 2:00 am

Posted in Uncategorized

©2010-2012      

Built on a Wordpress using a customization of the Journalist theme.