1"""
2Support for Key-Value Coding in Python. This provides a simple functional
3interface to Cocoa's Key-Value coding that also works for regular Python
4objects.
5
6Public API:
7
8    setKey(obj, key, value) -> None
9    setKeyPath (obj, keypath, value) -> None
10
11    getKey(obj, key) -> value
12    getKeyPath (obj, keypath) -> value
13
14A keypath is a string containing a sequence of keys seperated by dots. The
15path is followed by repeated calls to 'getKey'. This can be used to easily
16access nested attributes.
17
18This API is mirroring the 'getattr' and 'setattr' APIs in Python, this makes
19it more natural to work with Key-Value coding from Python. It also doesn't
20require changes to existing Python classes to make use of Key-Value coding,
21making it easier to build applications as a platform independent core with
22a Cocoa GUI layer.
23
24See the Cocoa documentation on the Apple developer website for more
25information on Key-Value coding. The protocol is basicly used to enable
26weaker coupling between the view and model layers.
27"""
28from __future__ import unicode_literals
29import sys
30
31__all__ = ("getKey", "setKey", "getKeyPath", "setKeyPath")
32if sys.version_info[0] == 2:
33    __all__ = tuple(str(x) for x in __all__)
34
35
36import objc
37import types
38import sys
39import collections
40import warnings
41
42if sys.version_info[0] == 2:
43    from itertools import imap as map
44    pass
45
46else:   # pragma: no cover (py3k)
47    basestring = str
48
49_null = objc.lookUpClass('NSNull').null()
50
51def keyCaps(s):
52    return s[:1].capitalize() + s[1:]
53
54# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/393090
55# Title: Binary floating point summation accurate to full precision
56# Version no: 2.2
57
58def msum(iterable):
59    "Full precision summation using multiple floats for intermediate values"
60    # sorted, non-overlapping partial sums
61    partials = []
62    for x in iterable:
63        i = 0
64        for y in partials:
65            if abs(x) < abs(y):
66                x, y = y, x
67            hi = x + y
68            lo = y - (hi - x)
69            if lo:
70                partials[i] = lo
71                i += 1
72            x = hi
73        partials[i:] = [x]
74    return sum(partials, 0.0)
75
76class _ArrayOperators (object):
77
78    @staticmethod
79    def avg(obj, segments):
80        path = '.'.join(segments)
81        lst = getKeyPath(obj, path)
82        count = len(lst)
83        if count == 0:
84            return 0.0
85        return msum(float(x) if x is not _null else 0.0 for x in lst) / count
86
87    @staticmethod
88    def count(obj, segments):
89        return len(obj)
90
91    @staticmethod
92    def distinctUnionOfArrays(obj, segments):
93        path = '.'.join(segments)
94        rval = []
95        s = set()
96        r = []
97        for lst in obj:
98            for item in [ getKeyPath(item, path) for item in lst ]:
99                try:
100                    if item in s or item in r:
101                        continue
102                    rval.append(item)
103                    s.add(item)
104
105                except TypeError:
106                    if item in rval:
107                        continue
108
109                    rval.append(item)
110                    r.append(item)
111        return rval
112
113    @staticmethod
114    def distinctUnionOfSets(obj, segments):
115        path = '.'.join(segments)
116        rval = set()
117        for lst in obj:
118            for item in [ getKeyPath(item, path) for item in lst ]:
119                rval.add(item)
120        return rval
121
122    @staticmethod
123    def distinctUnionOfObjects(obj, segments):
124        path = '.'.join(segments)
125        rval = []
126        s = set()
127        r = []
128        lst = [ getKeyPath(item, path) for item in obj ]
129        for item in lst:
130            try:
131                if item in s or item in r:
132                    continue
133
134                rval.append(item)
135                s.add(item)
136
137            except TypeError:
138                if item in rval:
139                    continue
140
141                rval.append(item)
142                r.append(item)
143        return rval
144
145
146    @staticmethod
147    def max(obj, segments):
148        path = '.'.join(segments)
149        return max(x for x in getKeyPath(obj, path) if x is not _null)
150
151    @staticmethod
152    def min(obj, segments):
153        path = '.'.join(segments)
154        return min(x for x in getKeyPath(obj, path) if x is not _null)
155
156    @staticmethod
157    def sum(obj, segments):
158        path = '.'.join(segments)
159        lst = getKeyPath(obj, path)
160        return msum(float(x) if x is not _null else 0.0 for x in lst)
161
162    @staticmethod
163    def unionOfArrays(obj, segments):
164        path = '.'.join(segments)
165        rval = []
166        for lst in obj:
167            rval.extend([ getKeyPath(item, path) for item in lst ])
168        return rval
169
170
171    @staticmethod
172    def unionOfObjects(obj, segments):
173        path = '.'.join(segments)
174        return [ getKeyPath(item, path) for item in obj]
175
176
177
178
179def getKey(obj, key):
180    """
181    Get the attribute referenced by 'key'. The key is used
182    to build the name of an attribute, or attribute accessor method.
183
184    The following attributes and accesors are tried (in this order):
185
186    - Accessor 'getKey'
187    - Accesoor 'get_key'
188    - Accessor or attribute 'key'
189    - Accessor or attribute 'isKey'
190    - Attribute '_key'
191
192    If none of these exist, raise KeyError
193    """
194    if obj is None:
195        return None
196    if isinstance(obj, (objc.objc_object, objc.objc_class)):
197        return obj.valueForKey_(key)
198
199    # check for dict-like objects
200    getitem = getattr(obj, '__getitem__', None)
201    if getitem is not None:
202        try:
203            return getitem(key)
204        except (KeyError, IndexError, TypeError):
205            pass
206
207    # check for array-like objects
208    if isinstance(obj, (collections.Sequence, collections.Set)) and not isinstance(obj, (basestring, collections.Mapping)):
209        def maybe_get(obj, key):
210            try:
211                return getKey(obj, key)
212            except KeyError:
213                return _null
214        return [maybe_get(obj, key) for obj in iter(obj)]
215
216    try:
217        m = getattr(obj, "get" + keyCaps(key))
218    except AttributeError:
219        pass
220    else:
221        return m()
222
223    try:
224        m = getattr(obj, "get_" + key)
225    except AttributeError:
226        pass
227    else:
228        return m()
229
230    for keyName in (key, "is" + keyCaps(key)):
231        try:
232            m = getattr(obj, keyName)
233        except AttributeError:
234            continue
235
236        if isinstance(m, types.MethodType) and m.__self__ is obj:
237            return m()
238
239        elif isinstance(m, types.BuiltinMethodType):
240            # Can't access the bound self of methods of builtin classes :-(
241            return m()
242
243        elif isinstance(m, objc.selector) and m.self is obj:
244            return m()
245
246        else:
247            return m
248
249    try:
250        return getattr(obj, "_" + key)
251    except AttributeError:
252        raise KeyError("Key %s does not exist" % (key,))
253
254
255def setKey(obj, key, value):
256    """
257    Set the attribute referenced by 'key' to 'value'. The key is used
258    to build the name of an attribute, or attribute accessor method.
259
260    The following attributes and accessors are tried (in this order):
261    - Mapping access (that is __setitem__ for collection.Mapping instances)
262    - Accessor 'setKey_'
263    - Accessor 'setKey'
264    - Accessor 'set_key'
265    - Attribute '_key'
266    - Attribute 'key'
267
268    Raises KeyError if the key doesn't exist.
269    """
270    if obj is None:
271        return
272    if isinstance(obj, (objc.objc_object, objc.objc_class)):
273        obj.setValue_forKey_(value, key)
274        return
275
276    if isinstance(obj, collections.Mapping):
277        obj[key] = value
278        return
279
280    aBase = 'set' + keyCaps(key)
281    for accessor in (aBase + '_', aBase, 'set_' + key):
282        m = getattr(obj, accessor, None)
283        if m is None:
284            continue
285        try:
286            m(value)
287            return
288        except TypeError:
289            pass
290
291    try:
292        m = getattr(obj, key)
293    except AttributeError:
294        pass
295
296    else:
297        if isinstance(m, types.MethodType) and m.__self__ is obj:
298            # This looks like a getter method, don't call setattr
299            pass
300
301        else:
302            try:
303                setattr(obj, key, value)
304                return
305            except AttributeError:
306                raise KeyError("Key %s does not exist" % (key,))
307
308    try:
309        getattr(obj, "_" + key)
310    except AttributeError:
311        pass
312    else:
313        setattr(obj, "_" + key, value)
314        return
315
316    try:
317        setattr(obj, key, value)
318    except AttributeError:
319        raise KeyError("Key %s does not exist" % (key,))
320
321def getKeyPath(obj, keypath):
322    """
323    Get the value for the keypath. Keypath is a string containing a
324    path of keys, path elements are seperated by dots.
325    """
326    if not keypath:
327        raise KeyError
328
329    if obj is None:
330        return None
331
332
333    if isinstance(obj, (objc.objc_object, objc.objc_class)):
334        return obj.valueForKeyPath_(keypath)
335
336    elements = keypath.split('.')
337    cur = obj
338    elemiter = iter(elements)
339    for e in elemiter:
340        if e[:1] == '@':
341            try:
342                oper = getattr(_ArrayOperators, e[1:])
343            except AttributeError:
344                raise KeyError("Array operator %s not implemented" % (e,))
345            return oper(cur, elemiter)
346        cur = getKey(cur, e)
347    return cur
348
349def setKeyPath(obj, keypath, value):
350    """
351    Set the value at 'keypath'. The keypath is a string containing a
352    path of keys, seperated by dots.
353    """
354    if obj is None:
355        return
356
357    if isinstance(obj, (objc.objc_object, objc.objc_class)):
358        return obj.setValue_forKeyPath_(value, keypath)
359
360    elements = keypath.split('.')
361    cur = obj
362    for e in elements[:-1]:
363        cur = getKey(cur, e)
364
365    return setKey(cur, elements[-1], value)
366
367
368class kvc(object):
369    def __init__(self, obj):
370        self.__pyobjc_object__ = obj
371
372    def __getattr__(self, attr):
373        return getKey(self.__pyobjc_object__, attr)
374
375    def __repr__(self):
376        return repr(self.__pyobjc_object__)
377
378    def __setattr__(self, attr, value):
379        if not attr.startswith('_'):
380            setKey(self.__pyobjc_object__, attr, value)
381
382        else:
383            object.__setattr__(self, attr, value)
384
385    def __getitem__(self, item):
386        if not isinstance(item, basestring):
387            raise TypeError('Keys must be strings')
388        return getKeyPath(self.__pyobjc_object__, item)
389
390    def __setitem__(self, item, value):
391        if not isinstance(item, basestring):
392            raise TypeError('Keys must be strings')
393        setKeyPath(self.__pyobjc_object__, item, value)
394
395
396
397# XXX: Undocumented and deprecated functions, only present because these had public
398#      names in previous releases and the module has suboptimal documentation.
399#      To be removed in PyObjC 3.0
400class ArrayOperators (_ArrayOperators):
401    def __init__(self):
402        warnings.warn("Don't use PyObjCTools.KeyValueCoding.ArrayOperators", DeprecationWarning)
403
404class _Deprecated (object):
405    def __getattr__(self, nm):
406        warnings.warn("Don't use PyObjCTools.KeyValueCoding.arrayOperators", DeprecationWarning)
407        return getattr(_ArrayOperators, nm)
408
409arrayOperators = _Deprecated()
410