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"""
28
29__all__ = ("getKey", "setKey", "getKeyPath", "setKeyPath")
30
31import objc
32import types
33from itertools import imap
34try:
35    set
36except NameError:
37    from sets import Set as set
38
39if objc.lookUpClass('NSObject').alloc().init().respondsToSelector_('setValue:forKey:'):
40    SETVALUEFORKEY = 'setValue_forKey_'
41    SETVALUEFORKEYPATH = 'setValue_forKeyPath_'
42else:
43    SETVALUEFORKEY = 'takeValue_forKey_'
44    SETVALUEFORKEYPATH = 'takeValue_forKeyPath_'
45
46def keyCaps(s):
47    return s[:1].capitalize() + s[1:]
48
49# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/393090
50# Title: Binary floating point summation accurate to full precision
51# Version no: 2.2
52
53def msum(iterable):
54    "Full precision summation using multiple floats for intermediate values"
55    # sorted, non-overlapping partial sums
56    partials = []
57    for x in iterable:
58        i = 0
59        for y in partials:
60            if abs(x) < abs(y):
61                x, y = y, x
62            hi = x + y
63            lo = y - (hi - x)
64            if lo:
65                partials[i] = lo
66                i += 1
67            x = hi
68        partials[i:] = [x]
69    return sum(partials, 0.0)
70
71class ArrayOperators(object):
72    def avg(self, obj, segments):
73        path = u'.'.join(segments)
74        lst = getKeyPath(obj, path)
75        count = len(lst)
76        if count == 0:
77            return 0.0
78        return msum(imap(float, lst)) / count
79
80    def count(self, obj, segments):
81        return len(obj)
82
83    def distinctUnionOfArrays(self, obj, segments):
84        path = u'.'.join(segments)
85        rval = []
86        s = set()
87        lists = getKeyPath(obj, path)
88        for lst in lists:
89            for item in lst:
90                if item in s:
91                    continue
92                rval.append(item)
93                s.add(item)
94        return rval
95
96    def distinctUnionOfObjects(self, obj, segments):
97        path = u'.'.join(segments)
98        rval = []
99        s = set()
100        lst = getKeyPath(obj, path)
101        for item in lst:
102            if item in s:
103                continue
104            rval.append(item)
105            s.add(item)
106        return rval
107
108    def max(self, obj, segments):
109        path = u'.'.join(segments)
110        return max(getKeyPath(obj, path))
111
112    def min(self, obj, segments):
113        path = u'.'.join(segments)
114        return min(getKeyPath(obj, path))
115
116    def sum(self, obj, segments):
117        path = u'.'.join(segments)
118        lst = getKeyPath(obj, path)
119        return msum(imap(float, lst))
120
121    def unionOfArrays(self, obj, segments):
122        path = u'.'.join(segments)
123        rval = []
124        lists = getKeyPath(obj, path)
125        for lst in lists:
126            rval.extend(lst)
127        return rval
128
129    def unionOfObjects(self, obj, segments):
130        path = u'.'.join(segments)
131        return getKeyPath(obj, path)
132
133arrayOperators = ArrayOperators()
134
135def getKey(obj, key):
136    """
137    Get the attribute referenced by 'key'. The key is used
138    to build the name of an attribute, or attribute accessor method.
139
140    The following attributes and accesors are tried (in this order):
141
142    - Accessor 'getKey'
143    - Accesoor 'get_key'
144    - Accessor or attribute 'key'
145    - Accessor or attribute 'isKey'
146    - Attribute '_key'
147
148    If none of these exist, raise KeyError
149    """
150    if obj is None:
151        return None
152    if isinstance(obj, (objc.objc_object, objc.objc_class)):
153        try:
154            return obj.valueForKey_(key)
155        except ValueError, msg:
156            # This is not entirely correct, should check if this
157            # is the right kind of ValueError before translating
158            raise KeyError, str(msg)
159
160    # check for dict-like objects
161    getitem = getattr(obj, '__getitem__', None)
162    if getitem is not None:
163        try:
164            return getitem(key)
165        except (KeyError, IndexError, TypeError):
166            pass
167
168    # check for array-like objects
169    if not isinstance(obj, basestring):
170        try:
171            itr = iter(obj)
172        except TypeError:
173            pass
174        else:
175            return [getKey(obj, key) for obj in itr]
176
177    try:
178        m = getattr(obj, "get" + keyCaps(key))
179    except AttributeError:
180        pass
181    else:
182        return m()
183
184    try:
185        m = getattr(obj, "get_" + key)
186    except AttributeError:
187        pass
188    else:
189        return m()
190
191    for keyName in (key, "is" + keyCaps(key)):
192        try:
193            m = getattr(obj, keyName)
194        except AttributeError:
195            continue
196
197        if isinstance(m, types.MethodType) and m.im_self is obj:
198            return m()
199
200        elif isinstance(m, types.BuiltinMethodType):
201            # Can't access the bound self of methods of builtin classes :-(
202            return m()
203
204        elif isinstance(m, objc.selector) and m.self is obj:
205            return m()
206
207        else:
208            return m
209
210    try:
211        return getattr(obj, "_" + key)
212    except AttributeError:
213        raise KeyError, "Key %s does not exist" % (key,)
214
215
216def setKey(obj, key, value):
217    """
218    Set the attribute referenced by 'key' to 'value'. The key is used
219    to build the name of an attribute, or attribute accessor method.
220
221    The following attributes and accessors are tried (in this order):
222    - Accessor 'setKey_'
223    - Accessor 'setKey'
224    - Accessor 'set_key'
225    - Attribute '_key'
226    - Attribute 'key'
227
228    Raises KeyError if the key doesn't exist.
229    """
230    if obj is None:
231        return
232    if isinstance(obj, (objc.objc_object, objc.objc_class)):
233        try:
234            getattr(obj, SETVALUEFORKEY)(value, key)
235            return
236        except ValueError, msg:
237            raise KeyError, str(msg)
238
239    aBase = 'set' + keyCaps(key)
240    for accessor in (aBase + '_', aBase, 'set_' + key):
241        m = getattr(obj, accessor, None)
242        if m is None:
243            continue
244        try:
245            m(value)
246            return
247        except TypeError:
248            pass
249
250    try:
251        o = getattr(obj, "_" + key)
252    except AttributeError:
253        pass
254    else:
255        setattr(obj, "_" + key, value)
256        return
257
258    try:
259        setattr(obj, key, value)
260    except AttributeError:
261        raise KeyError, "Key %s does not exist" % (key,)
262
263def getKeyPath(obj, keypath):
264    """
265    Get the value for the keypath. Keypath is a string containing a
266    path of keys, path elements are seperated by dots.
267    """
268    if obj is None:
269        return None
270
271    if isinstance(obj, (objc.objc_object, objc.objc_class)):
272        return obj.valueForKeyPath_(keypath)
273
274    elements = keypath.split('.')
275    cur = obj
276    elemiter = iter(elements)
277    for e in elemiter:
278        if e[:1] == u'@':
279            try:
280                oper = getattr(arrayOperators, e[1:])
281            except AttributeError:
282                raise KeyError, "Array operator %s not implemented" % (e,)
283            return oper(cur, elemiter)
284        cur = getKey(cur, e)
285    return cur
286
287def setKeyPath(obj, keypath, value):
288    """
289    Set the value at 'keypath'. The keypath is a string containing a
290    path of keys, seperated by dots.
291    """
292    if obj is None:
293        return
294
295    if isinstance(obj, (objc.objc_object, objc.objc_class)):
296        return getattr(obj, SETVALUEFORKEYPATH)(value, keypath)
297
298    elements = keypath.split('.')
299    cur = obj
300    for e in elements[:-1]:
301        cur = getKey(cur, e)
302
303    return setKey(cur, elements[-1], value)
304
305
306class kvc(object):
307    def __init__(self, obj):
308        self.__pyobjc_object__ = obj
309
310    def __getattr__(self, attr):
311        return getKey(self.__pyobjc_object__, attr)
312
313    def __repr__(self):
314        return repr(self.__pyobjc_object__)
315
316    def __setattr__(self, attr, value):
317        if not attr.startswith('_'):
318            setKey(self.__pyobjc_object__, attr, value)
319        object.__setattr__(self, attr, value)
320
321    def __getitem__(self, item):
322        if not isinstance(item, basestring):
323            raise TypeError, 'Keys must be strings'
324        return getKeyPath(self.__pyobjc_object__, item)
325
326    def __setitem__(self, item, value):
327        if not isinstance(item, basestring):
328            raise TypeError, 'Keys must be strings'
329        setKeyPath(self.__pyobjc_object__, item, value)
330