Commits

Ronald Oussoren  committed d20aa56

Implement tests for the various property types
and fix issues uncovered by those tests.

As noted before objc.set_property, objc.array_property
and objc.dict_property are still fairly minimal in that
they don't provide hooks for overriding methods.

  • Participants
  • Parent commits 6e376ee
  • Branches pyobjc-ancient

Comments (0)

Files changed (6)

File pyobjc-core/Lib/objc/_properties.py

 class array_proxy (collections.MutableSequence):
     # XXX: The implemenation should be complete, but is currently not
     # tested.
-    __slots__ = ('_name', '_wrapped', '_parent', '_ro')
+    __slots__ = ('_name', '_parent', '__wrapped', '_ro')
 
     def __init__(self, name, parent, wrapped, read_only):
-        self._wrapped = wrapped
         self._name = name
         self._parent = parent
         self._ro = read_only
+        self.__wrapped = wrapped
 
+    @property
+    def _wrapped(self):
+        return self.__wrapped.__getvalue__(self._parent)
+
+    @_wrapped.setter
+    def _wrapped(self, value):
+        setattr(self._parent, self._name, value)
 
     def __indexSetForIndex(self, index):
         if isinstance(index, slice):
 
 
     def __repr__(self):
-        return '<array proxy for property ' + self._name + repr(self._wrapped) + '>'
+        return '<array proxy for property ' + self._name + ' ' + repr(self._wrapped) + '>'
 
     def __reduce__(self):
         # Ensure that the proxy itself doesn't get stored
 
     def __set__(self, object, value):
         if isinstance(value, array_property):
-            print "set1", object, value
             value = list(value)
-            print "set2", object, value
 
         super(array_property, self).__set__(object, value)
 
         if v is None:
             v = list()
             object_property.__set__(self, object, v)
-        return array_proxy(self._name, object, v, self._ro)
+        return array_proxy(self._name, object, self, self._ro)
 
-NSKeyValueUnionSetMutation = 1,
-NSKeyValueMinusSetMutation = 2,
-NSKeyValueIntersectSetMutation = 3,
+    def __getvalue__(self, object):
+        v = object_property.__get__(self, object, None)
+        if v is None:
+            v = list()
+            object_property.__set__(self, object, v)
+        return v
+
+
+NSKeyValueUnionSetMutation = 1
+NSKeyValueMinusSetMutation = 2
+NSKeyValueIntersectSetMutation = 3
 NSKeyValueSetSetMutation = 4
              
 
-class set_proxy (object):
-    __slots__ = ('_name', '_wrapped', '_parent', '_ro')
+class set_proxy (collections.MutableSet):
+    __slots__ = ('_name', '__wrapped', '_parent', '_ro')
 
-    def __init__(cls, name, parent, wrapped, read_only):
-        v = cls.alloc().init()
-        v._name = name
-        v._wrapped = wrapped
-        v._parent = parent
-        v._ro = read_only
+    def __init__(self, name, parent, wrapped, read_only):
+        self._name = name
+        self._parent = parent
+        self._ro = read_only
+        self.__wrapped = wrapped
+
+    def __repr__(self):
+        return '<set proxy for property ' + self._name + ' ' + repr(self._wrapped) + '>'
+
+    @property
+    def _wrapped(self):
+        return self.__wrapped.__getvalue__(self._parent)
+
+    @_wrapped.setter
+    def _wrapped(self, value):
+        setattr(self._parent, self._name, value)
 
     def __getattr__(self, attr):
-        return getattr(self.wrapped, attr)
+        return getattr(self._wrapped, attr)
+
+
+    def __contains__(self, value):
+        return self._wrapped.__contains__(value)
+    
+    def __iter__(self):
+        return self._wrapped.__iter__()
+    
+    def __len__(self):
+        return self._wrapped.__len__()
+
+
+    def __eq__(self, other):
+        if isinstance(other, set_proxy):
+            return self._wrapped == other._wrapped
+
+        else:
+            return self._wrapped == other
+
+    def __ne__(self, other):
+        if isinstance(other, set_proxy):
+            return self._wrapped != other._wrapped
+
+        else:
+            return self._wrapped != other
+
+    def __lt__(self, other):
+        if isinstance(other, set_proxy):
+            return self._wrapped < other._wrapped
+
+        else:
+            return self._wrapped < other
+
+    def __le__(self, other):
+        if isinstance(other, set_proxy):
+            return self._wrapped <= other._wrapped
+
+        else:
+            return self._wrapped <= other
+
+    def __gt__(self, other):
+        if isinstance(other, set_proxy):
+            return self._wrapped > other._wrapped
+
+        else:
+            return self._wrapped > other
+
+    def __ge__(self, other):
+        if isinstance(other, set_proxy):
+            return self._wrapped >= other._wrapped
+
+        else:
+            return self._wrapped >= other
+
+
+    if sys.version_info[0] == 2:
+        def __cmp__(self, other):
+            if isinstance(other, set_proxy):
+                return cmp(self._wrapped, other._wrapped)
+
+            else:
+                return cmp(self._wrapped, other)
 
     def add(self, item):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueUnionSetMutation,
-                set(item),
+                set([item]),
         )
         try:
             self._wrapped.add(item)
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueUnionSetMutation,
-                set(item),
+                set([item]),
             )
 
     def clear(self):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         object = set(self._wrapped)
         self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
         try:
             self._wrapped.clear()
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueMinusSetMutation,
                 object
             )
 
     def difference_update(self, *others):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         s = set()
         s.update(*others)
         self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
             self._wrapped.difference_update(s)
 
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueMinusSetMutation,
                 s
 
 
     def discard(self, item):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueMinusSetMutation,
-                set(item)
+                set([item])
         )
         try:
-            self._wrapped.discard(s)
+            self._wrapped.discard(item)
 
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueMinusSetMutation,
-                set(item)
+                set([item])
             )
         
     def intersection_update(self, other):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueIntersectSetMutation,
-                set(item)
+                set([item])
         )
         try:
             self._wrapped.intersection_update(s)
 
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueIntersectSetMutation,
-                set(item)
+                set([item])
             )
 
     def pop(self):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         try:
             v = iter(self).next()
         except KeyError:
 
 
     def remove(self, item):
-        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
-                self._name,
-                NSKeyValueMinusSetMutation,
-                set(item)
-        )
-        try:
-            self._wrapped.remove(s)
-
-        finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
-                self._name,
-                NSKeyValueMinusSetMutation,
-                set(item)
-            )
-
-    def symmetric_difference_update(self, other):
-        other = set(other)
-        s = set()
-        for item in other:
-            if item in self:
-                s.add(item)
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
 
         self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueMinusSetMutation,
-                s
+                set([item])
         )
         try:
-            self._wrapped.symmetric_difference_update(other)
+            self._wrapped.remove(item)
 
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueMinusSetMutation,
-                s
+                set([item])
+            )
+
+    def symmetric_difference_update(self, other):
+        # NOTE: This method does not call the corresponding method
+        # of the wrapped set to ensure that we generate the right
+        # notifications.
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
+        other = set(other)
+
+        to_add = set()
+        to_remove = set()
+        for o in other:
+            if o in self:
+                to_remove.add(o)
+            else:
+                to_add.add(o)
+
+        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+                self._name,
+                NSKeyValueMinusSetMutation,
+                to_remove
+        )
+        try:
+            self._wrapped.difference_update(to_remove)
+
+        finally:
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
+                    self._name,
+                    NSKeyValueMinusSetMutation,
+                    to_remove
+            )
+
+        self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+                self._name,
+                NSKeyValueUnionSetMutation,
+                to_add
+        )
+        try:
+            self._wrapped.update(to_add)
+
+        finally:
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
+                    self._name,
+                    NSKeyValueUnionSetMutation,
+                    to_add
             )
 
     def update(self, *others):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         s = set()
         s.update(*others)
 
             self._wrapped.update(s)
 
         finally:
-            self._parent.willChangeValueForKey_withSetMutation_usingObjects_(
+            self._parent.didChangeValueForKey_withSetMutation_usingObjects_(
                 self._name,
                 NSKeyValueUnionSetMutation,
                 s
             )
 
+    def __or__(self, other):
+        return self._wrapped | other
+
+    def __and__(self, other):
+        return self._wrapped & other
+
+    def __xor__(self, other):
+        return self._wrapped ^ other
+
+    def __sub__(self, other):
+        return self._wrapped - other
 
     def __ior__(self, other):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         return self|other
 
     def __isub__(self, other):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         return self-other
 
     def __ixor__(self, other):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         return self^other
 
     def __iand__(self, other):
+        if self._ro:
+            raise ValueError("Property '%s' is read-only"%(self._name,))
+
         return self&other
 
 
 
 class set_property (object_property):
-    def __get__(self):
-        v = object_property.__get__(self)
+    def __init__(self, name=None, 
+            read_only=False, copy=True, dynamic=False, 
+            ivar=None, depends_on=None):
+        super(set_property, self).__init__(name, 
+                read_only=read_only, 
+                copy=copy, dynamic=dynamic,
+                ivar=ivar, depends_on=depends_on)
+
+    def __get__(self, object, owner):
+        v = object_property.__get__(self, object, owner)
         if v is None:
             v = set()
-            object_property.__set__(self, v)
-        return set_proxy(self._name, v, self._ro)
+            object_property.__set__(self, object, v)
+        return set_proxy(self._name, object, self, self._ro)
+
+    def __getvalue__(self, object):
+        v = object_property.__get__(self, object, None)
+        if v is None:
+            v = set()
+            object_property.__set__(self, object, v)
+        return v
 
 
 NSMutableDictionary = lookUpClass('NSMutableDictionary')
 
 class dict_property (object_property):
-    def __get__(self):
-        v = object_property.__get__(self)
+    def __get__(self, object, owner):
+        v = object_property.__get__(self, object, owner)
         if v is None:
             v = NSMutableDictionary.alloc().init()
-            object_property.__set__(self, v)
-        return dict_proxy(self._name, v, self._ro)
+            object_property.__set__(self, object, v)
+        return object_property.__get__(self, object, owner)
+        

File pyobjc-core/PyObjCTest/loader.py

 
 
 def makeTestSuite():
-    topdir = dirname(dirname(__file__))
+    import __main__
+    topdir = dirname(__main__.__file__)
     if sys.version_info[0] == 3:
         del sys.path[1]
         deja_topdir = dirname(dirname(topdir))

File pyobjc-core/PyObjCTest/test_array_property.py

             self.fail("ValueError not raised")
 
 
+    def testAssingmentInteraction(self):
+        o = TestArrayPropertyHelper.alloc().init()
+        array = o.array
+
+        o.array.append(1)
+        self.assertEquals(len(o.array), 1)
+        self.assertEquals(len(array), 1)
 
 if __name__ == "__main__":
     main()

File pyobjc-core/PyObjCTest/test_dict_property.py

 
 from PyObjCTools.TestSupport import *
+from test_object_property import OCObserve
+import objc
 
+NSObject = objc.lookUpClass('NSObject')
+
+class TestDictPropertyHelper (NSObject):
+    aDict = objc.dict_property()
 
 class TestDictProperty (TestCase):
-    def testMissing(self):
-        self.fail("Implement tests")
+    # objc.dict_property is currently just an object_property with a default value
 
+    def testDefault(self):
+        observer = OCObserve.alloc().init()
+
+        o = TestDictPropertyHelper.alloc().init()
+        observer.register(o, 'aDict')
+        try:
+            self.assertEquals(len(o.aDict), 0)
+            self.assertEquals(o.aDict, {})
+            self.assertEquals(len(observer.values), 0)
+
+            o.aDict['key'] = 42
+            self.assertEquals(len(observer.values), 0)
+
+            observer.register(o, 'aDict.key')
+
+            o.aDict['key'] = 43
+            self.assertEquals(len(observer.values), 1)
+
+            self.assertNotIn('indexes', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], 42)
+            self.assertEquals(observer.values[-1][-1]['new'], 43)
+
+            del o.aDict['key']
+
+            self.assertEquals(len(observer.values), 2)
+            self.assertNotIn('indexes', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], 43)
+            self.assertEquals(observer.values[-1][-1]['new'], None)
+
+        finally:
+            observer.unregister(o, 'aDict')
+            observer.unregister(o, 'aDict.key')
 
 if __name__ == "__main__":
     main()

File pyobjc-core/PyObjCTest/test_object_property.py

     def init(self):
         self = super(OCObserve, self).init()
         self.values = []
+        self.registrations = []
         return self
 
     def register(self, object, keypath):
         object.addObserver_forKeyPath_options_context_(
                 self, keypath, 0x3, None)
+        self.registrations.append((object, keypath))
 
     def unregister(self, object, keypath):
         object.removeObserver_forKeyPath_(self, keypath)
             self, keypath, object, change, context):
         self.values.append((object, keypath, change))
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        for o, k in self.registrations:
+            self.unregister(o, k)
+        self.registrations = []
+
 
 class TestObjectProperty (TestCase):
     def testCreation(self):

File pyobjc-core/PyObjCTest/test_set_property.py

 from PyObjCTools.TestSupport import *
+import objc
+from test_object_property import OCObserve
 
+NSObject = objc.lookUpClass('NSObject')
+
+class TestSetPropertyHelper (NSObject):
+    aSet = objc.set_property()
 
 class TestSetProperty (TestCase):
-    def testMissing(self):
-        self.fail("Implement tests")
+    def testCopying(self):
+        o = TestSetPropertyHelper.alloc().init()
+        aSet = set([1,2])
+        o.aSet = aSet
 
+        self.assertEquals(len(o.aSet), 2)
+        self.assertEquals(len(aSet), 2)
+        aSet.add(3)
+        self.assertEquals(len(o.aSet), 2)
+        self.assertEquals(len(aSet), 3)
+
+    def testDefault(self):
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+
+            observer.register(o, 'aSet')
+
+            o.aSet = set()
+            self.assertEquals(len(observer.values), 1)
+
+            self.assertEquals(len(o.aSet), 0)
+            o.aSet.add('a')
+            o.aSet.add('b')
+            self.assertEquals(len(o.aSet), 2)
+            self.assertEquals(len(observer.values), 3)
+
+            self.assertEquals(observer.values[-2][-1]['kind'], 2)
+            self.assertNotIn('old', observer.values[-2][-1])
+            self.assertEquals(observer.values[-2][-1]['new'], set('a'))
+
+            self.assertEquals(observer.values[-1][-1]['kind'], 2)
+            self.assertNotIn('old', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['new'], set('b'))
+
+            v = list(o.aSet)
+            v.sort()
+            self.assertEquals(v, ['a', 'b'])
+
+            self.assertEqual(o.aSet, set(['a', 'b']))
+
+            proxy = o.aSet
+
+
+            o.aSet.clear()
+            self.assertEquals(len(o.aSet), 0)
+            self.assertEquals(len(proxy), 0)
+
+            self.assertEquals(len(observer.values), 4)
+            self.assertEquals(observer.values[-1][-1]['old'], set(['a', 'b']))
+            self.assertNotIn('new', observer.values[-1][-1])
+
+    def testDifferenceUpdate(self):
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+
+            o.aSet.add(1)
+            o.aSet.add(2)
+            o.aSet.add(3)
+
+            observer.register(o, 'aSet')
+
+            o.aSet.difference_update(set([1,4]))
+            self.assertEquals(o.aSet, set([2,3]))
+
+            self.assertEquals(len(observer.values), 1)
+            self.assertEquals(observer.values[-1][-1]['kind'], 3)
+            self.assertNotIn('new', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], set([1]))
+
+    def testSymmetricDifferenceUpdate(self):
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+
+            o.aSet = set([1,2,3])
+
+            observer.register(o, 'aSet')
+
+            self.assertEquals(len(observer.values), 0)
+            o.aSet.symmetric_difference_update(set([1,4]))
+            self.assertEquals(o.aSet, set([2, 3, 4]))
+
+            self.assertEquals(len(observer.values), 2)
+
+            haveAdd = False
+            haveRemove = False
+
+            for i in 0, 1:
+                if observer.values[i][-1]['kind'] == 3:
+                    # Remove
+                    self.assertNotIn('new', observer.values[i][-1])
+                    self.assertEquals(observer.values[i][-1]['old'], set([1]))
+                    haveRemove = True
+
+                elif observer.values[i][-1]['kind'] == 2:
+                    # Remove
+                    self.assertNotIn('old', observer.values[i][-1])
+                    self.assertEquals(observer.values[i][-1]['new'], set([4]))
+                    haveAdd = True
+
+                else:
+                    self.fail("Unexpected update kind")
+
+            if not haveAdd and haveRemove:
+                self.fail("Do not have both an add and remove notification")
+
+    def testUpdate(self):
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+            observer.register(o, 'aSet')
+
+            o.aSet.update([1,2,3])
+            self.assertEquals(len(observer.values), 1)
+            self.assertEquals(observer.values[-1][-1]['kind'], 2)
+            self.assertNotIn('old', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['new'], set([1, 2, 3]))
+
+            o.aSet.update(set([3,4,5]))
+            self.assertEquals(len(observer.values), 2)
+            self.assertEquals(observer.values[-1][-1]['kind'], 2)
+            self.assertNotIn('old', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['new'], set([4, 5]))
+
+
+
+    def testAddDiscard(self): 
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+            observer.register(o, 'aSet')
+
+            o.aSet.add(1)
+            self.assertEquals(len(observer.values), 1)
+            self.assertNotIn('old', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['new'], set([1]))
+
+            o.aSet.discard(1)
+            self.assertEquals(len(observer.values), 2)
+            self.assertNotIn('new', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], set([1]))
+
+            o.aSet.discard(2)
+            self.assertEquals(len(observer.values), 3)
+            self.assertNotIn('new', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], set([]))
+
+
+    def testAddRemove(self): 
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+            observer.register(o, 'aSet')
+
+            o.aSet.add(1)
+            self.assertEquals(len(observer.values), 1)
+            self.assertNotIn('old', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['new'], set([1]))
+
+            o.aSet.remove(1)
+            self.assertEquals(len(observer.values), 2)
+            self.assertNotIn('new', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], set([1]))
+
+            self.assertRaises(KeyError, o.aSet.remove, 2)
+            self.assertEquals(len(observer.values), 3)
+            self.assertNotIn('new', observer.values[-1][-1])
+            self.assertEquals(observer.values[-1][-1]['old'], set([]))
+
+    def testInplace(self):
+        with OCObserve.alloc().init() as observer:
+            o = TestSetPropertyHelper.alloc().init()
+            o.aSet.add(1)
+            observer.register(o, 'aSet')
+
+
+            # The inplace operators aren't actually implemented, which means
+            # "o.aSet |= value" is actually  "o.aSet = o.aSet | value" and
+            # we get the some KVO notificatons as when a new value is set.
+
+            o.aSet |= set([2,3])
+            self.assertEquals(o.aSet, set([1,2,3]))
+            self.assertEquals(len(observer.values), 1)
+            self.assertEquals(observer.values[-1][-1]['kind'], 1)
+            self.assertEquals(observer.values[-1][-1]['old'], set([1]))
+            self.assertEquals(observer.values[-1][-1]['new'], set([1,2,3]))
+
+            o.aSet &= set([3, 4])
+            self.assertEquals(o.aSet, set([3]))
+            self.assertEquals(len(observer.values), 2)
+            self.assertEquals(observer.values[-1][-1]['kind'], 1)
+            self.assertEquals(observer.values[-1][-1]['old'], set([1,2,3]))
+            self.assertEquals(observer.values[-1][-1]['new'], set([3]))
+
+            o.aSet -= set([3])
+            self.assertEquals(o.aSet, set([]))
+            self.assertEquals(len(observer.values), 3)
+            self.assertEquals(observer.values[-1][-1]['kind'], 1)
+            self.assertEquals(observer.values[-1][-1]['old'], set([3]))
+            self.assertEquals(observer.values[-1][-1]['new'], set())
+
+            o.aSet = set([1,2,3])
+            self.assertEquals(len(observer.values), 4)
+
+            o.aSet ^= set([1, 4])
+            self.assertEquals(o.aSet, set([2, 3, 4]))
+            self.assertEquals(len(observer.values), 5)
+            self.assertEquals(observer.values[-1][-1]['kind'], 1)
+            self.assertEquals(observer.values[-1][-1]['old'], set([1,2,3]))
+            self.assertEquals(observer.values[-1][-1]['new'], set([2,3,4]))
 
 if __name__ == "__main__":
     main()