[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