Commits

Ronald Oussoren committed f7a4207

Improve coverage for objc._properties, and fix some bugs found by the new tests

Comments (0)

Files changed (4)

pyobjc-core/Lib/objc/_properties.py

         self._setter = None
         self._validate = None
         if depends_on is None:
-            self._depends_on = ()
+            self._depends_on = None
         else:
-            self._depends_on = list(depends_on)
+            self._depends_on = set(depends_on)
 
         self.__getprop = None
         self.__setprop = None
         self.__parent = None
 
     def _clone(self):
+        if self._depends_on is None:
+            depends = None
+        else:
+            depends = self._depends_on.copy()
+
         v = type(self)(name=self._name, 
                 read_only=self._ro, copy=self._copy, dynamic=self._dynamic,
-                ivar=self._ivar, typestr=self._typestr, depends_on=None)
+                ivar=self._ivar, typestr=self._typestr, depends_on=depends)
         v.__inherit = True
 
         v.__getprop = self.__getprop
                             "Cannot create default setter for property "
                             "without ivar")
 
-                    self.__setprop = selector(
+                    setprop = selector(
                         attrsetter(self._name, ivname, self._copy),
                         selector=setterName,
                         signature=signature
                     )
-                    self.__setprop.isHidden = True
-                    instance_methods.add(self.__setprop)
+                    setprop.isHidden = True
+                    instance_methods.add(setprop)
+
+                    # Use dynamic setter to avoid problems when subclassing
+                    self.__setprop = _dynamic_setter(setterName)
             else:
-                self.__setprop = selector(
+                setprop = selector(
                     self._setter,
                     selector=setterName,
                     signature=signature
                 )
-                self.__setprop.isHidden = True
-                instance_methods.add(self.__setprop)
+                setprop.isHidden = True
+                instance_methods.add(setprop)
+
+                # Use dynamic setter to avoid problems when subclassing
+                self.__setprop = _dynamic_setter(setterName)
 
         if self._typestr in (_C_NSBOOL, _C_BOOL):
-            getterName = b'is' + name[0].upper().encode('latin1') + name[:1].encode('latin1')
+            getterName = b'is' + name[0].upper().encode('latin1') + name[1:].encode('latin1')
         else:
             getterName = self._name.encode('latin1')
 
 
             elif self._dynamic:
                 if self._typestr in (_C_NSBOOL, _C_BOOL):
-                    dynGetterName = 'is' + name[0].upper() + name[:1]
+                    dynGetterName = 'is' + name[0].upper() + name[1:]
                 else:
                     dynGetterName = self._name
 
                 instance_methods.add(self.__getprop)
 
         else:
-            self.__getprop = selector(
+            self.__getprop = getprop = selector(
                     self._getter,
                     selector=getterName,
                     signature=self._typestr + b'@:')
-            self.__getprop.isHidden=True
-            instance_methods.add(self.__getprop)
+            getprop.isHidden=True
+            instance_methods.add(getprop)
+            #self.__getprop = _dynamic_getter(getterName)
 
         if self._validate is not None:
             selName = b'validate' + self._name[0].upper().encode('latin') + self._name[1:].encode('latin') + b':error:'
                     self._validate,
                     selector=selName,
                     signature=signature)
-            validate.isHidden = True
+            class_dict[validate.selector] = validate
             instance_methods.add(validate)
 
         if self._depends_on:
                     selector = b'keyPathsForValuesAffecting' + self._name[0].upper().encode('latin1') + self._name[1:].encode('latin1'),
                     signature = b'@@:',
                     isClassMethod=True)
-            affecting.isHidden = True
             class_dict[affecting.selector] = affecting
             class_methods.add(affecting)
 
-
     def __get__(self, object, owner):
         if object is None:
             return self
 
 
 
-def _id(value):
-    return value
 
 NSIndexSet = lookUpClass('NSIndexSet')
 NSMutableIndexSet = lookUpClass('NSMutableIndexSet')
 NSKeyValueChangeRemoval = 3
 NSKeyValueChangeReplacement = 4
 
+
+# Helper function for (not) pickling array_proxy instances
+# NOTE: Don't remove this function, it can be referenced from
+# pickle files.
+def _id(value):
+    return value
+
 # FIXME: split into two: array_proxy and mutable_array_proxy
 class array_proxy (collections.MutableSequence):
     # XXX: The implemenation should be complete, but is currently not

pyobjc-core/NEWS.txt

   code that creates the block is compiled using a recent enough compiler (although "recent
   enough" is fairly old by now)
 
+- Fixes some issues with :class:`objc.object_property` which were found by
+  improved unittests. In particular:
+
+  - The selector names for boolean properties were wrong
+
+  - Properties with a "depends_on" list didn't inherit properly
+
+  - Properties that were used in subclasses didn't generate the correct KVO 
+    events when they were observed.
+
+  - KVO issues with computed (read-only) properties
 
 Version 2.4.1
 -------------

pyobjc-core/PyObjCTest/test_object_property.py

 
     def observeValueForKeyPath_ofObject_change_context_(
             self, keypath, object, change, context):
-        self.values.append((object, keypath, change))
+
+        # We don't get to keep the 'change' dictionary, make
+        # a copy (it gets reused in future calls)
+        self.values.append((object, keypath, dict(change)))
 
     def __enter__(self):
         return self
 
             @p3.getter
             def p3(self):
-                return (self.p1, self.p2)
+                return (self.p1 or '', self.p2 or '')
 
-        observer = OCObserve.alloc().init()
-        object = OCTestObjectProperty2.alloc().init()
+        class OCTestObjectProperty2b (OCTestObjectProperty2):
+            p4 = objc.object_property()
 
-        observer.register(object, 'p1')
-        observer.register(object, 'p2')
-        observer.register(object, 'p3')
+            @OCTestObjectProperty2.p3.getter
+            def p3(self):
+                return (self.p4 or '', self.p2 or '', self.p1 or '')
+            p3.depends_on('p4')
+
+        observer1 = OCObserve.alloc().init()
+        observer2 = OCObserve.alloc().init()
+        object1 = OCTestObjectProperty2.alloc().init()
+        object2 = OCTestObjectProperty2b.alloc().init()
+
+        v = type(object1).keyPathsForValuesAffectingP3()
+        self.assertIsInstance(v, objc.lookUpClass('NSSet'))
+        self.assertEqual(v, {'p1', 'p2'})
+
+        v = type(object2).keyPathsForValuesAffectingP3()
+        self.assertIsInstance(v, objc.lookUpClass('NSSet'))
+        self.assertEqual(v, {'p1', 'p2', 'p4'})
+
+        self.assertTrue(object1.respondsToSelector('p1'))
+        self.assertTrue(object1.respondsToSelector('setP1:'))
+        self.assertTrue(object1.respondsToSelector('p2'))
+        self.assertTrue(object1.respondsToSelector('setP2:'))
+        self.assertTrue(object1.respondsToSelector('p3'))
+        self.assertFalse(object1.respondsToSelector('setP3:'))
+
+        self.assertTrue(object2.respondsToSelector('p1'))
+        self.assertTrue(object2.respondsToSelector('setP1:'))
+        self.assertTrue(object2.respondsToSelector('p2'))
+        self.assertTrue(object2.respondsToSelector('setP2:'))
+        self.assertTrue(object2.respondsToSelector('p3'))
+        self.assertFalse(object2.respondsToSelector('setP3:'))
+        self.assertTrue(object2.respondsToSelector('p4'))
+        self.assertTrue(object2.respondsToSelector('setP4:'))
+
+        observer1.register(object1, 'p1')
+        observer1.register(object1, 'p2')
+        observer1.register(object1, 'p3')
+
+        observer2.register(object2, 'p1')
+        observer2.register(object2, 'p2')
+        observer2.register(object2, 'p3')
+        observer2.register(object2, 'p4')
+
         try:
+            self.assertEqual(observer1.values, [])
+            self.assertEqual(observer2.values, [])
 
-            self.assertEqual(observer.values, [])
+            object1.p1 = "a"
+            object1.p2 = "b"
+            self.assertEqual(object1.p3, ("a", "b"))
+            self.assertEqual(object1.pyobjc_instanceMethods.p3(), ("a", "b"))
 
-            object.p1 = "a"
-            object.p2 = "b"
-            self.assertEqual(object.p3, ("a", "b"))
 
-            self.assertEqual(len(observer.values), 4)
+            object2.p1 = "a"
+            object2.p2 = "b"
+            object2.p4 = "c"
+            self.assertEqual(object2.p3, ("c", "b", "a"))
+            self.assertEqual(object2.pyobjc_instanceMethods.p3(), ("c", "b", "a"))
+            self.assertEqual(object2.pyobjc_instanceMethods.p4(), "c")
 
-            if observer.values[0][1] == 'p1':
-                self.assertEqual(observer.values[1][1], 'p3')
-            else:
-                self.assertEqual(observer.values[0][1], 'p3')
-                self.assertEqual(observer.values[1][1], 'p1')
+            seen = { v[1]: v[2]['new'] for v in observer1.values }
+            self.assertEqual(seen,
+                {'p1': 'a', 'p2': 'b', 'p3': ('a', 'b') })
 
-            if observer.values[2][1] == 'p2':
-                self.assertEqual(observer.values[3][1], 'p3')
-            else:
-                self.assertEqual(observer.values[2][1], 'p3')
-                self.assertEqual(observer.values[3][1], 'p2')
+            seen = { v[1]: v[2]['new'] for v in observer2.values }
+            self.assertEqual(seen,
+               {'p1': 'a', 'p2': 'b', 'p3': ('c', 'b', 'a'), 'p4': 'c' })
 
         finally:
-            observer.unregister(object, 'p1')
-            observer.unregister(object, 'p2')
-            observer.unregister(object, 'p3')
+            observer1.unregister(object1, 'p1')
+            observer1.unregister(object1, 'p2')
+            observer1.unregister(object1, 'p3')
+
+            observer2.unregister(object2, 'p1')
+            observer2.unregister(object2, 'p2')
+            observer2.unregister(object2, 'p3')
+            observer2.unregister(object2, 'p4')
+
+    def testDepends2(self):
+        class OCTestObjectProperty2B (NSObject):
+            p1 = objc.object_property()
+            @p1.getter
+            def p1(self):
+                return self._p1
+            @p1.setter
+            def p1(self, v):
+                self._p1 = v
+
+            p2 = objc.object_property()
+            @p2.getter
+            def p2(self):
+                return self._p2
+            @p2.setter
+            def p2(self, v):
+                self._p2 = v
+
+            p3 = objc.object_property(read_only=True, depends_on=['p1', 'p2'])
+
+            @p3.getter
+            def p3(self):
+                return (self.p1 or '', self.p2 or '')
+
+        class OCTestObjectProperty2Bb (OCTestObjectProperty2B):
+            p4 = objc.object_property()
+
+            @OCTestObjectProperty2B.p1.getter
+            def p1(self):
+                return self._p1
+
+            @OCTestObjectProperty2B.p3.getter
+            def p3(self):
+                return (self.p4 or '', self.p2 or '', self.p1 or '')
+            p3.depends_on('p4')
+
+        observer1 = OCObserve.alloc().init()
+        observer2 = OCObserve.alloc().init()
+        object1 = OCTestObjectProperty2B.alloc().init()
+        object2 = OCTestObjectProperty2Bb.alloc().init()
+
+        v = type(object1).keyPathsForValuesAffectingP3()
+        self.assertIsInstance(v, objc.lookUpClass('NSSet'))
+        self.assertEqual(v, {'p1', 'p2'})
+
+        v = type(object2).keyPathsForValuesAffectingP3()
+        self.assertIsInstance(v, objc.lookUpClass('NSSet'))
+        self.assertEqual(v, {'p1', 'p2', 'p4'})
+
+        self.assertTrue(object1.respondsToSelector('p1'))
+        self.assertTrue(object1.respondsToSelector('setP1:'))
+        self.assertTrue(object1.respondsToSelector('p2'))
+        self.assertTrue(object1.respondsToSelector('setP2:'))
+        self.assertTrue(object1.respondsToSelector('p3'))
+        self.assertFalse(object1.respondsToSelector('setP3:'))
+
+        self.assertTrue(object2.respondsToSelector('p1'))
+        self.assertTrue(object2.respondsToSelector('setP1:'))
+        self.assertTrue(object2.respondsToSelector('p2'))
+        self.assertTrue(object2.respondsToSelector('setP2:'))
+        self.assertTrue(object2.respondsToSelector('p3'))
+        self.assertFalse(object2.respondsToSelector('setP3:'))
+        self.assertTrue(object2.respondsToSelector('p4'))
+        self.assertTrue(object2.respondsToSelector('setP4:'))
+
+        observer1.register(object1, 'p1')
+        observer1.register(object1, 'p2')
+        observer1.register(object1, 'p3')
+
+        observer2.register(object2, 'p1')
+        observer2.register(object2, 'p2')
+        observer2.register(object2, 'p3')
+        observer2.register(object2, 'p4')
+
+        try:
+            self.assertEqual(observer1.values, [])
+            self.assertEqual(observer2.values, [])
+
+            object1.p1 = "a"
+            object1.p2 = "b"
+            self.assertEqual(object1.p3, ("a", "b"))
+            self.assertEqual(object1.pyobjc_instanceMethods.p3(), ("a", "b"))
+
+
+            object2.p1 = "a"
+            object2.p2 = "b"
+            object2.p4 = "c"
+            self.assertEqual(object2.p3, ("c", "b", "a"))
+            self.assertEqual(object2.pyobjc_instanceMethods.p3(), ("c", "b", "a"))
+            self.assertEqual(object2.pyobjc_instanceMethods.p4(), "c")
+
+            seen = { v[1]: v[2]['new'] for v in observer1.values }
+            self.assertEqual(seen,
+                {'p1': 'a', 'p2': 'b', 'p3': ('a', 'b') })
+
+            seen = { v[1]: v[2]['new'] for v in observer2.values }
+            self.assertEqual(seen,
+               {'p1': 'a', 'p2': 'b', 'p3': ('c', 'b', 'a'), 'p4': 'c' })
+
+        finally:
+            observer1.unregister(object1, 'p1')
+            observer1.unregister(object1, 'p2')
+            observer1.unregister(object1, 'p3')
+
+            observer2.unregister(object2, 'p1')
+            observer2.unregister(object2, 'p2')
+            observer2.unregister(object2, 'p3')
+            observer2.unregister(object2, 'p4')
 
     def testMethods(self):
         l = []
     def testDynamic(self):
         class OCTestObjectProperty8 (NSObject):
             p1 = objc.object_property(dynamic=True)
+            p2 = objc.object_property(dynamic=True, typestr=objc._C_NSBOOL)
 
         self.assertFalse(OCTestObjectProperty8.instancesRespondToSelector_(b"p1"))
         self.assertFalse(OCTestObjectProperty8.instancesRespondToSelector_(b"setP1:"))
+        self.assertFalse(OCTestObjectProperty8.instancesRespondToSelector_(b"isP2"))
+        self.assertFalse(OCTestObjectProperty8.instancesRespondToSelector_(b"setP2:"))
 
         v = [42]
         def getter(self):
         OCTestObjectProperty8.p1 = getter
         OCTestObjectProperty8.setP1_ = setter
 
+        v2 = [False]
+        def getter2(self):
+            return v2[0]
+        def setter2(self, value):
+            v2[0] = bool(value)
+        OCTestObjectProperty8.isP2 = getter2
+        OCTestObjectProperty8.setP2_ = setter2
+
+
         self.assertTrue(OCTestObjectProperty8.instancesRespondToSelector_(b"p1"))
         self.assertTrue(OCTestObjectProperty8.instancesRespondToSelector_(b"setP1:"))
+        self.assertTrue(OCTestObjectProperty8.instancesRespondToSelector_(b"isP2"))
+        self.assertTrue(OCTestObjectProperty8.instancesRespondToSelector_(b"setP2:"))
 
         o = OCTestObjectProperty8.alloc().init()
         self.assertIsInstance(OCTestObjectProperty8.p1, objc.object_property)
+        self.assertIsInstance(OCTestObjectProperty8.p2, objc.object_property)
 
 
         self.assertEqual(o.p1, 42)
+        self.assertEqual(o.p2, False)
         o.p1 = 99
+        o.p2 = True
         self.assertEqual(o.p1, 99)
         self.assertEqual(v[0], 99)
+        self.assertEqual(o.p2, True)
+        self.assertEqual(v2[0], True)
 
 
     def testReadOnly(self):
         class OCTestObjectProperty5 (NSObject):
             p1 = objc.object_property(read_only=True)
             p2 = objc.object_property()
+            p3 = objc.object_property(read_only=True, typestr=objc._C_NSBOOL)
 
         class OCTestObjectProperty6 (OCTestObjectProperty5):
             @OCTestObjectProperty5.p1.setter
             def p1(self, value):
                 self._p1 = value
 
+            @OCTestObjectProperty5.p2.setter
+            def p2(self, value):
+                self._p2 = value * 2
+
+            @OCTestObjectProperty5.p3.getter
+            def p3(self):
+                return not super(OCTestObjectProperty6, self).p3
+
         base = OCTestObjectProperty5.alloc().init()
         self.assertRaises(ValueError, setattr, base, 'p1', 1)
+        self.assertRaises(ValueError, setattr, base, 'p3', 1)
         base.p2 = 'b'
         self.assertEqual(base.p2, 'b')
 
         sub = OCTestObjectProperty6.alloc().init()
         sub.p1 = 1
         sub.p2 = 'a'
+        sub._p3 = False
         self.assertEqual(sub.p1, 1)
-        self.assertEqual(sub.p2, 'a')
+        self.assertEqual(sub.p2, 'aa')
+        self.assertEqual(sub.p3, True)
+
+        self.assertTrue(base.respondsToSelector_(b'p2'))
+        self.assertFalse(base.respondsToSelector_(b'setP1:'))
+        self.assertTrue(base.respondsToSelector_(b'isP3'))
+        self.assertFalse(base.respondsToSelector_(b'p3'))
+
+        self.assertTrue(sub.respondsToSelector_(b'p2'))
+        self.assertTrue(sub.respondsToSelector_(b'setP1:'))
+        self.assertTrue(sub.respondsToSelector_(b'isP3'))
+        self.assertFalse(sub.respondsToSelector_(b'p3'))
+
+    def testDefaultSetterWithoutIvar(self):
+
+        try:
+            class OCTestObjectProperty7 (NSObject):
+                p1 = objc.object_property(ivar=objc.NULL)
+        except ValueError:
+            pass
+
+        else:
+            self.fail("ValueError not raised")
+
+        try:
+            class OCTestObjectProperty8 (NSObject):
+                p1 = objc.object_property(ivar=objc.NULL, read_only=True)
+        except ValueError:
+            pass
+
+        else:
+            self.fail("ValueError not raised")
 
 if __name__ == "__main__":
     main()

pyobjc-framework-Cocoa/Doc/api-notes-kvo.txt

 ``didChangeValueForKey:`` when changing the attribute of an object that is
 a subclass of ``NSObject``. It is therefore not necessary to call those 
 methods in most use-cases for Key-Value Observing.
+
+.. warning::
+
+   The 'change' dictionary for ``observeValueForKeyPath:ofObject:change:context:``
+   can be changed after the method call, don't store a reference to this dictionary
+   but make a copy when you want to use its contents later on.