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