Ronald Oussoren avatar Ronald Oussoren committed 627a58c

Proper tests for the array operator functionality

Comments (0)

Files changed (3)

pyobjc-core/Lib/PyObjCTools/KeyValueCoding.py

 import objc
 import types
 import sys
+import collections
+import warnings
 
 if sys.version_info[0] == 2:
     from itertools import imap as map
     pass
 
-else:
+else:   # pragma: no cover (py3k)
     basestring = str
 
-if objc.lookUpClass('NSObject').alloc().init().respondsToSelector_('setValue:forKey:'):
-    SETVALUEFORKEY = 'setValue_forKey_'
-    SETVALUEFORKEYPATH = 'setValue_forKeyPath_'
-else:
-    SETVALUEFORKEY = 'takeValue_forKey_'
-    SETVALUEFORKEYPATH = 'takeValue_forKeyPath_'
-
-if sys.version_info[0] == 2:
-    SETVALUEFORKEY = str(SETVALUEFORKEY)
-    SETVALUEFORKEYPATH = str(SETVALUEFORKEYPATH)
+_null = objc.lookUpClass('NSNull').null()
 
 def keyCaps(s):
     return s[:1].capitalize() + s[1:]
         partials[i:] = [x]
     return sum(partials, 0.0)
 
-class ArrayOperators(object):
-    def avg(self, obj, segments):
+class _ArrayOperators (object):
+
+    @staticmethod
+    def avg(obj, segments):
         path = '.'.join(segments)
         lst = getKeyPath(obj, path)
         count = len(lst)
         if count == 0:
             return 0.0
-        return msum(map(float, lst)) / count
+        return msum(float(x) if x is not _null else 0.0 for x in lst) / count
 
-    def count(self, obj, segments):
+    @staticmethod
+    def count(obj, segments):
         return len(obj)
 
-    def distinctUnionOfArrays(self, obj, segments):
+    @staticmethod
+    def distinctUnionOfArrays(obj, segments):
         path = '.'.join(segments)
         rval = []
         s = set()
-        lists = getKeyPath(obj, path)
-        for lst in lists:
-            for item in lst:
-                if item in s:
-                    continue
-                rval.append(item)
-                s.add(item)
+        r = []
+        for lst in obj:
+            for item in [ getKeyPath(item, path) for item in lst ]:
+                try:
+                    if item in s or item in r:
+                        continue
+                    rval.append(item)
+                    s.add(item)
+
+                except TypeError:
+                    if item in rval:
+                        continue
+
+                    rval.append(item)
+                    r.append(item)
         return rval
 
-    def distinctUnionOfObjects(self, obj, segments):
+    @staticmethod
+    def distinctUnionOfSets(obj, segments):
+        path = '.'.join(segments)
+        rval = set()
+        for lst in obj:
+            for item in [ getKeyPath(item, path) for item in lst ]:
+                rval.add(item)
+        return rval
+
+    @staticmethod
+    def distinctUnionOfObjects(obj, segments):
         path = '.'.join(segments)
         rval = []
         s = set()
-        lst = getKeyPath(obj, path)
+        r = []
+        lst = [ getKeyPath(item, path) for item in obj ]
         for item in lst:
-            if item in s:
-                continue
-            rval.append(item)
-            s.add(item)
+            try:
+                if item in s or item in r:
+                    continue
+
+                rval.append(item)
+                s.add(item)
+
+            except TypeError:
+                if item in rval: 
+                    continue
+
+                rval.append(item)
+                r.append(item)
         return rval
 
-    def max(self, obj, segments):
+
+    @staticmethod
+    def max(obj, segments):
         path = '.'.join(segments)
-        return max(getKeyPath(obj, path))
+        return max(x for x in getKeyPath(obj, path) if x is not _null)
 
-    def min(self, obj, segments):
+    @staticmethod
+    def min(obj, segments):
         path = '.'.join(segments)
-        return min(getKeyPath(obj, path))
+        return min(x for x in getKeyPath(obj, path) if x is not _null)
 
-    def sum(self, obj, segments):
+    @staticmethod
+    def sum(obj, segments):
         path = '.'.join(segments)
         lst = getKeyPath(obj, path)
-        return msum(map(float, lst))
+        return msum(float(x) if x is not _null else 0.0 for x in lst)
 
-    def unionOfArrays(self, obj, segments):
+    @staticmethod
+    def unionOfArrays(obj, segments):
         path = '.'.join(segments)
         rval = []
-        lists = getKeyPath(obj, path)
-        for lst in lists:
-            rval.extend(lst)
+        for lst in obj:
+            rval.extend([ getKeyPath(item, path) for item in lst ])
         return rval
 
-    def unionOfObjects(self, obj, segments):
+
+    @staticmethod
+    def unionOfObjects(obj, segments):
         path = '.'.join(segments)
-        return getKeyPath(obj, path)
+        return [ getKeyPath(item, path) for item in obj]
 
-arrayOperators = ArrayOperators()
+
+
 
 def getKey(obj, key):
     """
             pass
 
     # check for array-like objects
-    if not isinstance(obj, basestring):
-        try:
-            itr = iter(obj)
-        except TypeError:
-            pass
-        else:
-            return [getKey(obj, key) for obj in itr]
+    if isinstance(obj, (collections.Sequence, collections.Set)) and not isinstance(obj, (basestring, collections.Mapping)):
+        def maybe_get(obj, key):
+            try:
+                return getKey(obj, key)
+            except KeyError:
+                return _null
+        return [maybe_get(obj, key) for obj in iter(obj)]
 
     try:
         m = getattr(obj, "get" + keyCaps(key))
         return
     if isinstance(obj, (objc.objc_object, objc.objc_class)):
         try:
-            getattr(obj, SETVALUEFORKEY)(value, key)
+            obj.setValue_forKey_(value, key)
             return
         except ValueError as msg:
             raise KeyError(str(msg))
     Get the value for the keypath. Keypath is a string containing a
     path of keys, path elements are seperated by dots.
     """
+    if not keypath:
+        raise KeyError
+
     if obj is None:
         return None
 
+
     if isinstance(obj, (objc.objc_object, objc.objc_class)):
         return obj.valueForKeyPath_(keypath)
 
     for e in elemiter:
         if e[:1] == '@':
             try:
-                oper = getattr(arrayOperators, e[1:])
+                oper = getattr(_ArrayOperators, e[1:])
             except AttributeError:
                 raise KeyError("Array operator %s not implemented" % (e,))
             return oper(cur, elemiter)
         return
 
     if isinstance(obj, (objc.objc_object, objc.objc_class)):
-        return getattr(obj, SETVALUEFORKEYPATH)(value, keypath)
+        return obj.setValue_forKeyPath_(value, keypath)
 
     elements = keypath.split('.')
     cur = obj
         if not isinstance(item, basestring):
             raise TypeError('Keys must be strings')
         setKeyPath(self.__pyobjc_object__, item, value)
+
+
+
+# XXX: Undocumented and deprecated functions, only present because these had public
+#      names in previous releases and the module has suboptimal documentation.
+#      To be removed in PyObjC 3.0
+class ArrayOperators (_ArrayOperators):
+    def __init__(self):
+        warnings.warn("Don't use PyObjCTools.KeyValueCoding.ArrayOperators", DeprecationWarning)
+
+class _Deprecated (object):
+    def __getattr__(self, nm):
+        warnings.warn("Don't use PyObjCTools.KeyValueCoding.arrayOperators", DeprecationWarning)
+        return getattr(_ArrayOperators, nm)
+
+arrayOperators = _Deprecated()

pyobjc-core/NEWS.txt

 - Fixed some issues with :class:`objc.array_property` and :class:`objc.set_property`
   that were found by much improved unittests.
 
+- Fixed issues with :mod:`PyObjCTools.KeyValueCoding` that were found by improved
+  unittests:
+
+  - ``getKey`` didn't work propertly on dictionaries (dictionaries were treated as sequences)
+
+  - ``getKeyPath(list, "@avg.field")`` didn't work when field wasn't a valid key for all
+     items in the list, and likewise for the '@sum', '@min', '@max' special keys.
+
+  - ``getKeyPath`` didn't raise the correct exception for empty key paths
+
+  - ``@unionOfObjects`` and ``@distinctUnionOfObjects`` operators for Python sequences
+    didn't raise an exception when the selected keypath didn't exist on an item of the sequence.
+
+  - ``@unionOfArrays`` and ``@distinctUnionOfArrays`` operators for Python sequences
+    didn't raise an exception when the selected keypath didn't exist on an item of the sequence.
+
+  - ``@distinctUnionOfArrays`` and ``@distinctUnionOfObjects`` didn't work properly when
+     the keypath pointed to objects that weren't hashable.
+
+  - ``@distinctUnionOfSets`` operator was not present at all. 
+
+
+- Removed Mac OS X 10.2 (!) compatibility from :mod:`PyObjCTools.KeyValueCoding`.
+
+- PyObjCTools.KeyValueCoding has undocumented attributes 'ArrayOperators' and 'arrayOperators',
+  both will be removed in a future release.
 
 
 Version 2.4.1

pyobjc-core/PyObjCTest/test_keyvaluecoding.py

 
 
 class TestArrayOperators (TestCase):
-    def test_missing(self):
-        self.fail("Missing tests for ArrayOperators")
+    def test_unknown_function(self):
+        values = [ { 'a': 1 } ]
+
+        self.assertRaises(KeyError, KeyValueCoding.getKeyPath, values, '@nofunction.a')
+
+
+    def test_sum(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        values = [
+                { 'a' : 1 },
+                { 'a' : 2, 'b': 4 },
+                { 'a' : 3, 'b': 2 },
+                { 'a' : 4 },
+        ]
+        self.assertEqual(arrayOperators.sum(values, 'a'), 10)
+        self.assertEqual(arrayOperators.sum(values, 'b'), 6)
+        self.assertEqual(arrayOperators.sum(values, 'c'), 0)
+        self.assertEqual(arrayOperators.sum([], 'b'), 0)
+        self.assertRaises(KeyError, arrayOperators.sum, [], ())
+
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@sum.a'), 10)
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@sum.b'), 6)
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@sum.c'), 0)
+
+
+    def test_avg(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        values = [
+                { 'a' : 1 },
+                { 'a' : 2, 'b': 4 },
+                { 'a' : 3, 'b': 2 },
+                { 'a' : 4 },
+        ]
+        self.assertEqual(arrayOperators.avg(values, 'a'), 2.5)
+        self.assertEqual(arrayOperators.avg(values, 'b'), 1.5)
+        self.assertEqual(arrayOperators.avg(values, 'c'), 0)
+        self.assertEqual(arrayOperators.avg([], 'b'), 0)
+        self.assertRaises(KeyError, arrayOperators.avg, [], ())
+
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@avg.a'), 2.5)
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@avg.b'), 1.5)
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@avg.c'), 0)
+
+    def test_count(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        values = [
+                { 'a' : 1 },
+                { 'a' : 2, 'b': 4 },
+                { 'a' : 3, 'b': 2 },
+                { 'a' : 4 },
+        ]
+        self.assertEqual(arrayOperators.count(values, 'a'), len(values))
+        self.assertEqual(arrayOperators.count(values, 'b'), len(values))
+        self.assertEqual(arrayOperators.count(values, ()), len(values))
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@count'), len(values))
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@count.a'), len(values))
+
+    def test_max(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        values = [
+                { 'a' : 1 },
+                { 'a' : 2, 'b': 5 },
+                { 'a' : 3, 'b': 2 },
+                { 'a' : 4 },
+        ]
+        self.assertEqual(arrayOperators.max(values, 'a'), 4)
+        self.assertEqual(arrayOperators.max(values, 'b'), 5)
+        self.assertRaises(KeyError, arrayOperators.max, values, ())
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@max.a'), 4)
+
+    def test_min(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        values = [
+                { 'a' : 1 },
+                { 'a' : 2, 'b': 5 },
+                { 'a' : 3, 'b': 2 },
+                { 'a' : 4 },
+        ]
+        self.assertEqual(arrayOperators.min(values, 'a'), 1)
+        self.assertEqual(arrayOperators.min(values, 'b'), 2)
+        self.assertRaises(KeyError, arrayOperators.min, values, ())
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@min.a'), 1)
+
+    def test_unionOfObjects(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        values = [
+                { 'a' : { 'b': 1 } },
+                { 'a' : { 'b': 1 } },
+                { 'a' : { 'b': 2 } },
+                { 'a' : { 'b': 3 } },
+        ]
+        
+        self.assertEqual(arrayOperators.unionOfObjects(values, ('a', 'b')), [1, 1, 2, 3 ])
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@unionOfObjects.a.b'), [1, 1, 2, 3])
+
+        values.append({'a': {}})
+        self.assertRaises(KeyError, arrayOperators.unionOfObjects, values, ('a', 'b'))
+
+    def test_distinctUnionOfObjects(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        class Int (object):
+            def __init__(self, value):
+                self._value = value
+
+            def __repr__(self):
+                return 'Int(%r)'%(self._value)
+
+            def __eq__(self, other):
+                if isinstance(other, int):
+                    return self._value == other
+
+                elif isinstance(other, Int):
+                    return self._value == other._value
+
+                else:
+                    return False
+
+            def __hash__(self): raise TypeError
+
+        values = [
+                { 'a' : { 'b': 1 } },
+                { 'a' : { 'b': Int(1) } },
+                { 'a' : { 'b': 2 } },
+                { 'a' : { 'b': Int(3) } },
+                { 'a' : { 'b': Int(3) } },
+        ]
+        
+        self.assertEqual(arrayOperators.distinctUnionOfObjects(values, ('a', 'b')), [1, 2, 3 ])
+        self.assertEqual(KeyValueCoding.getKeyPath(values, '@distinctUnionOfObjects.a.b'), [1, 2, 3 ])
+
+        values.append({'a': {}})
+        self.assertRaises(KeyError, arrayOperators.distinctUnionOfObjects, values, ('a', 'b'))
+        self.assertRaises(KeyError, KeyValueCoding.getKeyPath, values, '@distinctUnionOfObjects.a.b')
+
+        class Rec (object):
+            def __init__(self, b):
+                self.b = b
+
+            def __eq__(self, other):
+                return type(self) == type(other) and self.b == other.b
+
+            def __hash__(self): raise TypeError
+
+        values = [
+                { 'a' : Rec(1) },
+                { 'a' : Rec(1) },
+                { 'a' : Rec(2) },
+                { 'a' : Rec(3) },
+        ]
+        self.assertEqual(arrayOperators.distinctUnionOfObjects(values, ('a', 'b')), [1, 2, 3 ])
+
+    def test_unionOfArrays(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        class Rec (object):
+            def __init__(self, **kwds):
+                for k, v in kwds.items():
+                    setattr(self, k, v)
+
+            def __eq__(self, other):
+                return type(self) is type(other) and self.__dict__ == other.__dict__
+
+            def __hash__(self): raise TypeError
+
+        class Str (object):
+            def __init__(self, value):
+                self._value = value
+
+            def __repr__(self):
+                return 'Str(%r)'%(self._value)
+
+            def __eq__(self, other):
+                if isinstance(other, str):
+                    return self._value == other
+
+                elif isinstance(other, Str):
+                    return self._value == other._value
+
+                else:
+                    return False
+
+            def __cmp__(self, other):
+                if isinstance(other, str):
+                    return cmp(self._value, other)
+
+                elif isinstance(other, Str):
+                    return cmp(self._value, other._value)
+
+                else:
+                    return NotImplementedError
+
+            def __hash__(self): raise TypeError
+
+        transactions = [
+            [
+                dict(payee='Green Power', amount=120.0),
+                dict(payee='Green Power', amount=150.0),
+                dict(payee=Str('Green Power'), amount=170.0),
+                Rec(payee='Car Loan', amount=250.0),
+                dict(payee='Car Loan', amount=250.0),
+                dict(payee='Car Loan', amount=250.0),
+                dict(payee=Str('General Cable'), amount=120.0),
+                dict(payee='General Cable', amount=155.0),
+                Rec(payee='General Cable', amount=120.0),
+                dict(payee='Mortgage', amount=1250.0),
+                dict(payee='Mortgage', amount=1250.0),
+                dict(payee='Mortgage', amount=1250.0),
+                dict(payee='Animal Hospital', amount=600.0),
+            ],
+            [
+                dict(payee='General Cable - Cottage',   amount=120.0),
+                dict(payee='General Cable - Cottage',   amount=155.0),
+                Rec(payee='General Cable - Cottage',   amount=120.0),
+                dict(payee='Second Mortgage',   amount=1250.0),
+                dict(payee='Second Mortgage',   amount=1250.0),
+                dict(payee=Str('Second Mortgage'),   amount=1250.0),
+                dict(payee='Hobby Shop',   amount=600.0),
+            ]
+        ]
+
+        self.assertEqual(arrayOperators.distinctUnionOfArrays(transactions, ('payee',)), ['Green Power', 'Car Loan', 'General Cable', 'Mortgage', 'Animal Hospital', 'General Cable - Cottage', 'Second Mortgage', 'Hobby Shop'])
+        self.assertEqual(KeyValueCoding.getKeyPath(transactions, '@distinctUnionOfArrays.payee'), ['Green Power', 'Car Loan', 'General Cable', 'Mortgage', 'Animal Hospital', 'General Cable - Cottage', 'Second Mortgage', 'Hobby Shop'])
+        self.assertEqual(arrayOperators.unionOfArrays(transactions, ('payee',)), [
+            'Green Power', 
+            'Green Power', 
+            'Green Power', 
+            'Car Loan', 
+            'Car Loan', 
+            'Car Loan', 
+            'General Cable', 
+            'General Cable', 
+            'General Cable', 
+            'Mortgage', 
+            'Mortgage', 
+            'Mortgage', 
+            'Animal Hospital', 
+            'General Cable - Cottage', 
+            'General Cable - Cottage', 
+            'General Cable - Cottage', 
+            'Second Mortgage', 
+            'Second Mortgage', 
+            'Second Mortgage', 
+            'Hobby Shop'
+        ])
+        self.assertEqual(KeyValueCoding.getKeyPath(transactions, '@unionOfArrays.payee'), [
+            'Green Power', 
+            'Green Power', 
+            'Green Power', 
+            'Car Loan', 
+            'Car Loan', 
+            'Car Loan', 
+            'General Cable', 
+            'General Cable', 
+            'General Cable', 
+            'Mortgage', 
+            'Mortgage', 
+            'Mortgage', 
+            'Animal Hospital', 
+            'General Cable - Cottage', 
+            'General Cable - Cottage', 
+            'General Cable - Cottage', 
+            'Second Mortgage', 
+            'Second Mortgage', 
+            'Second Mortgage', 
+            'Hobby Shop'
+        ])
+
+        self.assertRaises(KeyError, arrayOperators.unionOfArrays, transactions, 'date')
+        self.assertRaises(KeyError, arrayOperators.distinctUnionOfArrays, transactions, 'date')
+
+    def testUnionOfSets(self):
+        arrayOperators = KeyValueCoding._ArrayOperators
+
+        class Rec (object):
+            def __init__(self, n):
+                self.n = n
+
+            def __eq__(self, other):
+                return self.n == other.n
+
+            def __hash__(self):
+                return hash(self.n)
+
+        
+        values = {
+            frozenset({
+                Rec(1),
+                Rec(1),
+                Rec(2),
+            }),
+            frozenset({
+                Rec(1),
+                Rec(3),
+            }),
+        }
+
+        self.assertEqual(arrayOperators.distinctUnionOfSets(values, 'n'), {1,2,3})
+
+class TestDeprecatedJunk (TestCase):
+    def test_deprecated_class (self):
+        with filterWarnings('error', DeprecationWarning):
+            self.assertRaises(DeprecationWarning, KeyValueCoding.ArrayOperators)
+
+
+        o = KeyValueCoding.ArrayOperators()
+        self.assertIsInstance(o, KeyValueCoding._ArrayOperators)
+
+    def test_deprecated_object (self):
+        with filterWarnings('error', DeprecationWarning):
+            self.assertRaises(DeprecationWarning, getattr, KeyValueCoding.arrayOperators, 'avg')
+
+        self.assertEqual(KeyValueCoding.arrayOperators.avg, KeyValueCoding._ArrayOperators.avg)
+
+
+
+null = objc.lookUpClass('NSNull').null()
 
 class TestPythonObject (TestCase):
     def test_missing(self):
         self.fail("Missing tests for KVC on Python objects")
 
+    def test_dict_get(self):
+        d = {'a':1 }
+        self.assertEqual(KeyValueCoding.getKey(d, 'a'), 1)
+        self.assertRaises(KeyError, KeyValueCoding.getKey, d, 'b')
+
+    def test_array_get(self):
+        l = [{'a': 1, 'b':2 }, {'a':2} ]
+        self.assertEquals(KeyValueCoding.getKey(l, 'a'), [1, 2])
+        self.assertEquals(KeyValueCoding.getKey(l, 'b'), [2, null])
+
+
 
 class TestObjectiveCObject (TestCase):
     def test_missing(self):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.