""" This module implements a callback function that is used by the C code to add Python special methods to Objective-C classes with a suitable interface. This module contains no user callable code. TODO: - Add external interface: Framework specific modules may want to add to this. - These are candidates for implementation: >>> from Foundation import * >>> set(dir(list)) - set(dir(NSMutableArray)) set(['__delslice__', '__imul__', '__getslice__', '__setslice__', '__iadd__', '__mul__', '__add__', '__rmul__']) >>> set(dir(dict)) - set(dir(NSMutableDictionary)) set(['__cmp__']) """ from _objc import setClassExtender, selector, lookUpClass, currentBundle, repythonify, splitSignature from itertools import imap __all__ = ['CONVENIENCE_METHODS', 'CLASS_METHODS'] CONVENIENCE_METHODS = {} CLASS_METHODS = {} NSObject = lookUpClass('NSObject') HAS_KVO = NSObject.instancesRespondToSelector_('willChangeValueForKey:') def isNative(sel): return not hasattr(sel, 'callable') def add_convenience_methods(super_class, name, type_dict): try: return _add_convenience_methods(super_class, name, type_dict) except: import traceback traceback.print_exc() raise def _add_convenience_methods(super_class, name, type_dict): """ Add additional methods to the type-dict of subclass 'name' of 'super_class'. CONVENIENCE_METHODS is a global variable containing a mapping from an Objective-C selector to a Python method name and implementation. CLASS_METHODS is a global variable containing a mapping from class name to a list of Python method names and implementation. Matching entries from both mappings are added to the 'type_dict'. """ if type_dict.get('__objc_python_subclass__'): if 'bundleForClass' not in type_dict: cb = currentBundle() def bundleForClass(cls): return cb type_dict['bundleForClass'] = selector(bundleForClass, isClassMethod=True) if (HAS_KVO and '__useKVO__' not in type_dict and isNative(type_dict.get('willChangeValueForKey_')) and isNative(type_dict.get('didChangeValueForKey_'))): useKVO = issubclass(super_class, NSObject) type_dict['__useKVO__'] = useKVO if '__bundle_hack__' in type_dict: import warnings warnings.warn( "__bundle_hack__ is not necessary in PyObjC 1.3+ / py2app 0.1.8+", DeprecationWarning) for k, sel in type_dict.items(): if not isinstance(sel, selector): continue if sel.selector == "alloc" or sel.selector == "allocWithZone:": sel.isAlloc = 1 if sel.selector.endswith(':error:'): sigParts = splitSignature(sel.signature) if sigParts[-1] == '^@': sigParts = sigParts[:-1] + ('o^@',) sel.signature = ''.join(sigParts) if sel.selector in ( 'copy', 'copyWithZone:', 'mutableCopy', 'mutableCopyWithZone:'): # These methods transfer ownership to the caller, the runtime uses # this information to adjust the reference count. sel.doesDonateReference = 1 sel = sel.selector if sel in CONVENIENCE_METHODS: v = CONVENIENCE_METHODS[sel] for nm, value in v: if nm in type_dict and isinstance(type_dict[nm], selector): # Clone attributes of already existing version t = type_dict[nm] v = selector(value, selector=t.selector, signature=t.signature, isClassMethod=t.isClassMethod) v.isAlloc = t.isAlloc type_dict[nm] = v else: type_dict[nm] = value if name in CLASS_METHODS: for nm, value in CLASS_METHODS[name]: type_dict[nm] = value setClassExtender(add_convenience_methods) def __getitem__objectForKey_(self, key): res = self.objectForKey_(container_wrap(key)) return container_unwrap(res, KeyError, key) def has_key_objectForKey_(self, key): res = self.objectForKey_(container_wrap(key)) return res is not None def get_objectForKey_(self, key, dflt=None): res = self.objectForKey_(container_wrap(key)) if res is None: res = dflt return res CONVENIENCE_METHODS['objectForKey:'] = ( ('__getitem__', __getitem__objectForKey_), ('has_key', has_key_objectForKey_), ('get', get_objectForKey_), ('__contains__', has_key_objectForKey_), ) def __delitem__removeObjectForKey_(self, key): self.removeObjectForKey_(container_wrap(key)) CONVENIENCE_METHODS['removeObjectForKey:'] = ( ('__delitem__', __delitem__removeObjectForKey_), ) def update_setObject_forKey_(self, other): # XXX - should this be more flexible? for key, value in other.items(): self[key] = value def setdefault_setObject_forKey_(self, key, dflt=None): try: return self[key] except KeyError: self[key] = dflt return dflt def __setitem__setObject_forKey_(self, key, value): self.setObject_forKey_(container_wrap(value), container_wrap(key)) def pop_setObject_forKey_(self, key, dflt=None): try: res = self[key] except KeyError: res = dflt else: del self[key] return res def popitem_setObject_forKey_(self): try: k = self[iter(self).next()] except StopIteration: raise KeyError, "popitem on an empty %s" % (type(self).__name__,) else: return (k, self[k]) CONVENIENCE_METHODS['setObject:forKey:'] = ( ('__setitem__', __setitem__setObject_forKey_), ('update', update_setObject_forKey_), ('setdefault', setdefault_setObject_forKey_), ('pop', pop_setObject_forKey_), ('popitem', popitem_setObject_forKey_), ) CONVENIENCE_METHODS['count'] = ( ('__len__', lambda self: self.count()), ) CONVENIENCE_METHODS['containsObject:'] = ( ('__contains__', lambda self, elem: bool(self.containsObject_(container_wrap(elem)))), ) CONVENIENCE_METHODS['hash'] = ( ('__hash__', lambda self: self.hash()), ) CONVENIENCE_METHODS['isEqualTo:'] = ( ('__eq__', lambda self, other: bool(self.isEqualTo_(other))), ) CONVENIENCE_METHODS['isEqual:'] = ( ('__eq__', lambda self, other: bool(self.isEqual_(other))), ) CONVENIENCE_METHODS['isGreaterThan:'] = ( ('__gt__', lambda self, other: bool(self.isGreaterThan_(other))), ) CONVENIENCE_METHODS['isGreaterThanOrEqualTo:'] = ( ('__ge__', lambda self, other: bool(self.isGreaterThanOrEqualTo_(other))), ) CONVENIENCE_METHODS['isLessThan:'] = ( ('__lt__', lambda self, other: bool(self.isLessThan_(other))), ) CONVENIENCE_METHODS['isLessThanOrEqualTo:'] = ( ('__le__', lambda self, other: bool(self.isLessThanOrEqualTo_(other))), ) CONVENIENCE_METHODS['isNotEqualTo:'] = ( ('__ne__', lambda self, other: bool(self.isNotEqualTo_(other))), ) CONVENIENCE_METHODS['length'] = ( ('__len__', lambda self: self.length()), ) CONVENIENCE_METHODS['addObject:'] = ( ('append', lambda self, item: self.addObject_(container_wrap(item))), ) def reverse_exchangeObjectAtIndex_withObjectAtIndex_(self): begin = 0 end = len(self) - 1 while begin < end: self.exchangeObjectAtIndex_withObjectAtIndex_(begin, end) begin += 1 end -= 1 CONVENIENCE_METHODS['exchangeObjectAtIndex:withObjectAtIndex:'] = ( ('reverse', reverse_exchangeObjectAtIndex_withObjectAtIndex_), ) def ensureArray(anArray): if not isinstance(anArray, (NSArray, list, tuple)): anArray = list(anArray) return anArray def extend_addObjectsFromArray_(self, anArray): self.addObjectsFromArray_(ensureArray(anArray)) CONVENIENCE_METHODS['addObjectsFromArray:'] = ( ('extend', extend_addObjectsFromArray_), ) def index_indexOfObject_(self, item): from Foundation import NSNotFound res = self.indexOfObject_(container_wrap(item)) if res == NSNotFound: raise ValueError, "%s.index(x): x not in list" % (type(self).__name__,) return res CONVENIENCE_METHODS['indexOfObject:'] = ( ('index', index_indexOfObject_), ) def insert_insertObject_atIndex_(self, idx, item): idx = slice(idx, None, None).indices(len(self)).start self.insertObject_atIndex_(container_wrap(item), idx) CONVENIENCE_METHODS['insertObject:atIndex:'] = ( ( 'insert', insert_insertObject_atIndex_), ) def __getitem__objectAtIndex_(self, idx): length = len(self) if isinstance(idx, slice): start, stop, step = idx.indices(length) #if step == 1: # m = getattr(self, 'subarrayWithRange_', None) # if m is not None: # return m((start, stop - start)) return [self[i] for i in xrange(start, stop, step)] if idx < 0: idx += length if idx < 0: raise IndexError, "index out of range" elif idx >= length: raise IndexError, "index out of range" return container_unwrap(self.objectAtIndex_(idx), RuntimeError) CONVENIENCE_METHODS['objectAtIndex:'] = ( ('__getitem__', __getitem__objectAtIndex_), ) def __delitem__removeObjectAtIndex_(self, idx): length = len(self) if isinstance(idx, slice): start, stop, step = idx.indices(length) if step == 1: if start > stop: start, stop = stop, start m = getattr(self, 'removeObjectsInRange_', None) if m is not None: m((start, stop - start)) return r = range(start, stop, step) r.sort() r.reverse() for i in r: self.removeObjectAtIndex_(i) return if idx < 0: idx += length if idx < 0: raise IndexError, "index out of range" elif idx >= length: raise IndexError, "index out of range" self.removeObjectAtIndex_(idx) def pop_removeObjectAtIndex_(self, idx=-1): length = len(self) if length <= 0: raise IndexError("pop from empty list") elif idx >= length or (idx + length) < 0: raise IndexError("pop index out of range") elif idx < 0: idx += length rval = self[idx] self.removeObjectAtIndex_(idx) return rval def remove_removeObjectAtIndex_(self, obj): idx = self.index(obj) self.removeObjectAtIndex_(idx) CONVENIENCE_METHODS['removeObjectAtIndex:'] = ( ('remove', remove_removeObjectAtIndex_), ('pop', pop_removeObjectAtIndex_), ('__delitem__', __delitem__removeObjectAtIndex_), ) def __setitem__replaceObjectAtIndex_withObject_(self, idx, anObject): length = len(self) if isinstance(idx, slice): start, stop, step = idx.indices(length) if step == 1: m = getattr(self, 'replaceObjectsInRange_withObjectsFromArray_', None) if m is not None: m((start, stop - start), ensureArray(anObject)) return # XXX - implement this.. raise NotImplementedError if idx < 0: idx += length if idx < 0: raise IndexError, "index out of range" elif idx >= length: raise IndexError, "index out of range" self.replaceObjectAtIndex_withObject_(idx, anObject) CONVENIENCE_METHODS['replaceObjectAtIndex:withObject:'] = ( ('__setitem__', __setitem__replaceObjectAtIndex_withObject_), ) def enumeratorGenerator(anEnumerator): while True: yield container_unwrap(anEnumerator.nextObject(), StopIteration) def dictItems(aDict): """ NSDictionary.items() """ keys = aDict.allKeys() return zip(keys, imap(aDict.__getitem__, keys)) CONVENIENCE_METHODS['allKeys'] = ( ('keys', lambda self: self.allKeys()), ('items', lambda self: dictItems(self)), ) CONVENIENCE_METHODS['allValues'] = ( ('values', lambda self: self.allValues()), ) def itemsGenerator(aDict): for key in aDict: yield (key, aDict[key]) def __iter__objectEnumerator_keyEnumerator(self): meth = getattr(self, 'keyEnumerator', None) if meth is None: meth = self.objectEnumerator return iter(meth()) CONVENIENCE_METHODS['keyEnumerator'] = ( ('__iter__', __iter__objectEnumerator_keyEnumerator), ('iterkeys', lambda self: iter(self.keyEnumerator())), ('iteritems', lambda self: itemsGenerator(self)), ) CONVENIENCE_METHODS['objectEnumerator'] = ( ('__iter__', __iter__objectEnumerator_keyEnumerator), ('itervalues', lambda self: iter(self.objectEnumerator())), ) CONVENIENCE_METHODS['reverseObjectEnumerator'] = ( ('__reversed__', lambda self: iter(self.reverseObjectEnumerator())), ) CONVENIENCE_METHODS['removeAllObjects'] = ( ('clear', lambda self: self.removeAllObjects()), ) CONVENIENCE_METHODS['dictionaryWithDictionary:'] = ( ('copy', lambda self: type(self).dictionaryWithDictionary_(self)), ) CONVENIENCE_METHODS['nextObject'] = ( ('__iter__', enumeratorGenerator), ) # # NSNumber seems to be and abstract base-class that is implemented using # NSCFNumber, a CoreFoundation 'proxy'. # NSNull = lookUpClass('NSNull') NSArray = lookUpClass('NSArray') null = NSNull.null() number_wrap = repythonify def container_wrap(v): if v is None: return null return v def container_unwrap(v, exc_type, *exc_args): if v is None: raise exc_type(*exc_args) elif v is null: return None return v # # Special wrappers for a number of varargs functions (constructors) # def initWithObjects_(self, *args): if args[-1] is not None: raise ValueError, "Need None as the last argument" return self.initWithArray_(args[:-1]) CONVENIENCE_METHODS['initWithObjects:'] = ( ('initWithObjects_', initWithObjects_), ) def arrayWithObjects_(self, *args): if args[-1] is not None: raise ValueError, "Need None as the last argument" return self.arrayWithArray_(args[:-1]) CONVENIENCE_METHODS['arrayWithObjects:'] = ( ('arrayWithObjects_', selector(arrayWithObjects_, signature='@@:@', isClassMethod=1)), ) def setWithObjects_(self, *args): if args[-1] is not None: raise ValueError, "Need None as the last argument" return self.setWithArray_(args[:-1]) CONVENIENCE_METHODS['setWithObjects:'] = ( ('setWithObjects_', selector(setWithObjects_, signature='@@:@', isClassMethod=1)), ) def setWithObjects_count_(self, args, count): return self.setWithArray_(args[:count]) CONVENIENCE_METHODS['setWithObjects:count:'] = ( ('setWithObjects_count_', selector(setWithObjects_count_, signature='@@:^@i', isClassMethod=1)), ) def splitObjectsAndKeys(values): if values[-1] is not None: raise ValueError, "Need None as the last argument" if len(values) % 2 != 1: raise ValueError, "Odd number of arguments" objects = [] keys = [] for i in range(0, len(values)-1, 2): objects.append(container_wrap(values[i])) keys.append(container_wrap(values[i+1])) return objects, keys def dictionaryWithObjectsAndKeys_(self, *values): objects, keys = splitObjectsAndKeys(values) return self.dictionaryWithObjects_forKeys_(objects, keys) CONVENIENCE_METHODS['dictionaryWithObjectsAndKeys:'] = ( ('dictionaryWithObjectsAndKeys_', selector(dictionaryWithObjectsAndKeys_, signature='@@:@',isClassMethod=1)), ) def fromkeys_dictionaryWithObjects_forKeys_(cls, keys, values=None): if not isinstance(keys, (list, tuple)): keys = list(keys) if values is None: values = (None,) * len(keys) elif not isinstance(values, (list, tuple)): values = list(values) return cls.dictionaryWithObjects_forKeys_(values, keys) CONVENIENCE_METHODS['dictionaryWithObjects:forKeys:'] = ( ('fromkeys', classmethod(fromkeys_dictionaryWithObjects_forKeys_)), ) def initWithObjectsAndKeys_(self, *values): objects, keys = splitObjectsAndKeys(values) return self.initWithObjects_forKeys_(objects, keys) CONVENIENCE_METHODS['initWithObjectsAndKeys:'] = ( ( 'initWithObjectsAndKeys_', initWithObjectsAndKeys_ ), ) def sort(self, cmpfunc=cmp): def doCmp(a, b, cmpfunc): return cmpfunc(a, b) self.sortUsingFunction_context_(doCmp, cmpfunc) CONVENIENCE_METHODS['sortUsingFunction:context:'] = ( ('sort', sort), ) CONVENIENCE_METHODS['hasPrefix:'] = ( ('startswith', lambda self, pfx: self.hasPrefix_(pfx)), ) CONVENIENCE_METHODS['hasSuffix:'] = ( ('endswith', lambda self, pfx: self.hasSuffix_(pfx)), ) CLASS_METHODS['NSNull'] = ( ('__nonzero__', lambda self: False ), ) NSDecimalNumber = lookUpClass('NSDecimalNumber') def _makeD(v): if isinstance(v, NSDecimalNumber): return v return NSDecimalNumber.decimalNumberWithDecimal_(v) CLASS_METHODS['NSDecimalNumber'] = ( ('__add__', lambda self, other: _makeD(self.decimalValue() + other)), ('__radd__', lambda self, other: _makeD(other + self.decimalValue())), ('__sub__', lambda self, other: _makeD(self.decimalValue() - other)), ('__rsub__', lambda self, other: _makeD(other - self.decimalValue())), ('__mul__', lambda self, other: _makeD(self.decimalValue() * other)), ('__rmul__', lambda self, other: _makeD(other * self.decimalValue())), ('__div__', lambda self, other: _makeD(self.decimalValue() / other)), ('__rdiv__', lambda self, other: _makeD(other / self.decimalValue())), ('__mod__', lambda self, other: _makeD(self.decimalValue() % other)), ('__rmod__', lambda self, other: _makeD(other % self.decimalValue())), ('__neg__', lambda self: _makeD(-(self.decimalValue()))), ('__pos__', lambda self: _makeD(+(self.decimalValue()))), ('__abs__', lambda self: _makeD(abs(self.decimalValue()))), ) def NSData__getslice__(self, i, j): return self.bytes()[i:j] def NSData__getitem__(self, item): buff = self.bytes() try: return buff[item] except TypeError: return buff[:][item] CLASS_METHODS['NSData'] = ( ('__str__', lambda self: self.bytes()[:]), ('__getitem__', NSData__getitem__), ('__getslice__', NSData__getslice__), ) def NSMutableData__setslice__(self, i, j, sequence): # XXX - could use replaceBytes:inRange:, etc. self.mutableBytes()[i:j] = sequence def NSMutableData__setitem__(self, item, value): self.mutableBytes()[item] = value CLASS_METHODS['NSMutableData'] = ( ('__setslice__', NSMutableData__setslice__), ('__setitem__', NSMutableData__setitem__), )