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