[Commits] (pje) Spike: add "validation event" support,
to avoid infinite regress when a
commits at osafoundation.org
commits at osafoundation.org
Mon Feb 21 17:50:26 PST 2005
Commit by: pje
Modified files:
internal/Spike/src/spike/models.py 1.3 1.4
internal/Spike/src/spike/models.txt 1.4 1.5
Log message:
Spike: add "validation event" support, to avoid infinite regress when a
partially-completed change on one side of a bidirectional relationship
would otherwise be unable to roll back.
ViewCVS links:
http://cvs.osafoundation.org/index.cgi/internal/Spike/src/spike/models.py.diff?r1=text&tr1=1.3&r2=text&tr2=1.4
http://cvs.osafoundation.org/index.cgi/internal/Spike/src/spike/models.txt.diff?r1=text&tr1=1.4&r2=text&tr2=1.5
Index: internal/Spike/src/spike/models.py
diff -u internal/Spike/src/spike/models.py:1.3 internal/Spike/src/spike/models.py:1.4
--- internal/Spike/src/spike/models.py:1.3 Tue Feb 15 16:36:34 2005
+++ internal/Spike/src/spike/models.py Mon Feb 21 17:50:24 2005
@@ -2,7 +2,7 @@
from events import Event
-__all__ = ['Set', 'SetChanged']
+__all__ = ['Set', 'SetChanged', 'Validation']
class Set(object):
@@ -38,6 +38,18 @@
"""Shortcut for ``SetChanged.getReceivers(set)``"""
return SetChanged.getReceivers(self)
+ def addValidator(self,validator,hold=False):
+ """Shortcut for ``Validation.subscribe(set,validator,hold)``"""
+ Validation.subscribe(self,validator,hold)
+
+ def removeValidator(self,validator):
+ """Shortcut for ``Validation.unsubscribe(set,validator)``"""
+ Validation.unsubscribe(self,validator)
+
+ def getValidators(self):
+ """Shortcut for ``Validation.getReceivers(set)``"""
+ return Validation.getReceivers(self)
+
def replace(self,remove=(),add=()):
"""Remove items in ``remove``, add items in ``add``, generate events"""
data = self.data
@@ -68,12 +80,16 @@
added.append(ob)
if removed or added:
+ evt = None
try:
- SetChanged(self,removed,added)
+ evt = Validation(self,removed,added)
+ SetChanged.send(evt) # forward event to change subscribers
except:
map(data.remove,added)
map(data.append,removed)
- SetChanged(self,added,removed)
+ if evt is not None:
+ # we sent a change event, so send a rollback
+ SetChanged(self,added,removed)
raise
def _typeName(self,t):
@@ -81,7 +97,7 @@
return '/'.join([typ.__name__ for typ in t]) or '()'
else:
return t.__name__
-
+
def __repr__(self):
r = "Set(%r)" % self.data
if self.type is not object:
@@ -101,11 +117,19 @@
__slots__ = 'added','removed'
def __init__(self,sender,removed,added,**kw):
- self.added = added
self.removed = removed
+ self.added = added
super(SetChanged,self).__init__(sender,**kw)
def __repr__(self):
return "<Change for %r: removed=%r, added=%r>" % (
self.sender, self.removed, self.added
)
+
+
+class Validation(SetChanged):
+ """Validate a just-made change"""
+ __slots__ = ()
+
+
+
Index: internal/Spike/src/spike/models.txt
diff -u internal/Spike/src/spike/models.txt:1.4 internal/Spike/src/spike/models.txt:1.5
--- internal/Spike/src/spike/models.txt:1.4 Mon Feb 21 09:17:25 2005
+++ internal/Spike/src/spike/models.txt Mon Feb 21 17:50:24 2005
@@ -23,13 +23,25 @@
Change Events
=============
-There is only one type of change event for sets: ``models.SetChanged``. The
-sender is the set that was changed, and its event instances have ``added``
-and ``removed`` attributes that are lists of the objects added or removed from
-the set::
-
- >>> models.SetChanged(None,[1,2],[3,4])
- <Change for None: removed=[1, 2], added=[3, 4]>
+There is are two types of change event for sets: ``models.Validation``, and
+``models.SetChanged``. Both types use similar event instances, with the
+``sender`` being the set that was changed, and the ``added`` and ``removed``
+attributes being iterables containing the objects added or removed from the
+set::
+
+ >>> models.Validation(models.Set(),[1,2],[3,4])
+ <Change for Set([]): removed=[1, 2], added=[3, 4]>
+
+ >>> models.SetChanged(models.Set(),[1,2],[3,4])
+ <Change for Set([]): removed=[1, 2], added=[3, 4]>
+
+The only practical difference between the two kinds of events is that
+subscribers to ``model.Validation`` events will receive the event before
+subscribers to ``model.SetChanged``. However, the same ``Validation`` instance
+is also sent to ``SetChanged`` subscribers, if none of the ``Validation``
+subscribers raised an error. This arrangement allows ``Validation`` listeners
+to reject a change (by raising an error) before any dependent changes are made
+by the ``SetChanged`` subscribers.
For demonstration purposes, we'll use this function to print change events as
they happen::
@@ -57,8 +69,20 @@
>>> models.SetChanged.getReceivers(s)
set([<weakref at ...; to 'function' at ... (printEvent)>])
-Since there is only one kind of event that sets produce, they have a simpler
-convenience API for managing subscriptions::
+Sets also have a simple convenience API for managing subscriptions to their
+``Validation`` and ``SetChanged``:: events
+
+ >>> s.addValidator(printEvent)
+ >>> models.Validation.getReceivers(s)
+ set([<weakref at ...; to 'function' at ... (printEvent)>])
+ >>> s.getValidators()
+ set([<weakref at ...; to 'function' at ... (printEvent)>])
+
+ >>> s.removeValidator(printEvent)
+ >>> s.getValidators()
+ set([])
+ >>> models.Validation.getReceivers(s)
+ set([])
>>> s.unsubscribe(printEvent)
>>> models.SetChanged.getReceivers(s)
@@ -83,7 +107,7 @@
>>> s
Set([1, 2])
-But adding the same object (as determined by ``==``) more than once has
+Adding the same object (as determined by ``==``) more than once has
no effect::
>>> s.add(1)
@@ -209,8 +233,8 @@
TypeError: Null set cannot be changed
-Rollback
-========
+Validation and Rollback
+=======================
If an event handler raises an exception during a change to a set, the change
is rolled back, and the set attempts to issue a change event that reverses the
@@ -222,13 +246,15 @@
>>> s = models.Set()
>>> def rejectMultiple(event):
+ ... print event
... if len(event.sender)>1:
... raise ValueError("Too many items added", event.added)
>>> s.subscribe(printEvent)
- >>> s.subscribe(rejectMultiple)
+ >>> s.addValidator(rejectMultiple)
- >>> s.add(1)
+ >>> s.add(1) # event printed by both printEvent and rejectMultiple
+ <Change for Set([1]): removed=[], added=[1]>
<Change for Set([1]): removed=[], added=[1]>
>>> s.add(2)
@@ -247,8 +273,29 @@
... except ValueError:
... pass
<Change for Set([1, 2]): removed=[], added=[2]>
+
+So there's the validation event that tried to add ``2``. However, since an
+error was raised before validation was complete, there was no ``SetChanged``
+event sent, and so it wasn't reversed. Let's move ``rejectMultiple`` so it
+only listens to ``SetChanged`` events, so we can see a rollback taking place::
+
+ >>> s.removeValidator(rejectMultiple)
+ >>> s.subscribe(rejectMultiple)
+ >>> s.unsubscribe(printEvent)
+
+ >>> try:
+ ... s.add(2)
+ ... except ValueError:
+ ... pass
+ <Change for Set([1, 2]): removed=[], added=[2]>
<Change for Set([1]): removed=[2], added=[]>
As you can see, an event is first generated for the add attempt, and then a
second event for the rollback.
+Notice, by the way, that rollback events are only generated if no validator
+rejected the change. This means that validators should *not* change system
+state, because if they do it will not be rolled back. Change listeners on the
+other hand, are free to make changes, but should avoid raising exceptions, in
+order to ensure that all change listeners receive the event.
+
More information about the Commits
mailing list