1"""
2This module implements a callback function that is used by the C code to
3add Python special methods to Objective-C classes with a suitable interface.
4
5This module contains no user callable code.
6
7TODO:
8- Add external interface: Framework specific modules may want to add to this.
9
10- These are candidates for implementation:
11
12    >>> from Foundation import *
13    >>> set(dir(list)) - set(dir(NSMutableArray))
14    set(['__delslice__', '__imul__', '__getslice__', '__setslice__',
15        '__iadd__', '__mul__', '__add__', '__rmul__'])
16    >>> set(dir(dict)) - set(dir(NSMutableDictionary))
17    set(['__cmp__'])
18
19"""
20from _objc import setClassExtender, selector, lookUpClass, currentBundle, repythonify, splitSignature, _block_call
21from itertools import imap
22import sys
23
24__all__ = ( 'addConvenienceForSelector', 'addConvenienceForClass' )
25
26
27# 12041508: NSDictionary now defines both objectForKey: and containsObject:,
28# so that the defining of __contains__ is order dependent (and often wrong).
29# So we define an OVERRIDE dict, whose keys are pipe-separated, triple strings:
30#
31# 1) the python method in question
32# 2) the first selector
33# 3) the overriding selector
34#
35# So "__contains__|containsObject:|objectForKey:" means that if containsObject:
36# was first, and has already defined __contains__, the objectForKey: can
37# later override the definition of __contains__ (but not in the reverse order).
38OVERRIDE = {'__contains__|containsObject:|objectForKey:': 1}
39def _canOverride(meth, owner, sel):
40    if not meth in owner:
41        return False
42    k = meth + '|' + owner[meth] + '|' + sel
43    return k in OVERRIDE
44
45CONVENIENCE_METHODS = {}
46CLASS_METHODS = {}
47
48def addConvenienceForSelector(selector, methods):
49    """
50    Add the list with methods to every class that has a selector with the
51    given name.
52    """
53    CONVENIENCE_METHODS[selector] = methods
54
55def addConvenienceForClass(classname, methods):
56    """
57    Add the list with methods to the class with the specified name
58    """
59    CLASS_METHODS[classname] = methods
60
61NSObject = lookUpClass('NSObject')
62
63def isNative(sel):
64    return not hasattr(sel, 'callable')
65
66def add_convenience_methods(super_class, name, type_dict):
67    try:
68        return _add_convenience_methods(super_class, name, type_dict)
69    except:
70        import traceback
71        traceback.print_exc()
72        raise
73
74def _add_convenience_methods(super_class, name, type_dict):
75    """
76    Add additional methods to the type-dict of subclass 'name' of
77    'super_class'.
78
79    CONVENIENCE_METHODS is a global variable containing a mapping from
80    an Objective-C selector to a Python method name and implementation.
81
82    CLASS_METHODS is a global variable containing a mapping from
83    class name to a list of Python method names and implementation.
84
85    Matching entries from both mappings are added to the 'type_dict'.
86    """
87    if type_dict.get('__objc_python_subclass__'):
88        if 'bundleForClass' not in type_dict:
89            cb = currentBundle()
90            def bundleForClass(cls):
91                return cb
92            type_dict['bundleForClass'] = selector(bundleForClass, isClassMethod=True)
93            if ('__useKVO__' not in type_dict and
94                    isNative(type_dict.get('willChangeValueForKey_')) and
95                    isNative(type_dict.get('didChangeValueForKey_'))):
96                useKVO = issubclass(super_class, NSObject)
97                type_dict['__useKVO__'] = useKVO
98        if '__bundle_hack__' in type_dict:
99            import warnings
100            warnings.warn(
101                "__bundle_hack__ is not necessary in PyObjC 1.3+ / py2app 0.1.8+",
102                DeprecationWarning)
103
104    owner = {}
105    for k, sel in type_dict.items():
106        if not isinstance(sel, selector):
107            continue
108
109        #
110        # Handle some common exceptions to the usual rules:
111        #
112
113        sel = sel.selector
114
115        if sel in CONVENIENCE_METHODS:
116            v = CONVENIENCE_METHODS[sel]
117            for nm, value in v:
118                if nm in type_dict and isinstance(type_dict[nm], selector):
119
120                    # Clone attributes of already existing version
121
122                    t = type_dict[nm]
123                    v = selector(value, selector=t.selector,
124                        signature=t.signature, isClassMethod=t.isClassMethod)
125
126                    type_dict[nm] = v
127                elif nm not in type_dict or _canOverride(nm, owner, sel):
128                    type_dict[nm] = value
129                    owner[nm] = sel
130
131    if name in CLASS_METHODS:
132        for nm, value in CLASS_METHODS[name]:
133            type_dict[nm] = value
134
135
136    if name == 'NSObject':
137        class kvc (object):
138            """
139            Key-Value-Coding accessor for Cocoa objects.
140
141            Both attribute access and dict-like indexing will attempt to
142            access the requested item through Key-Value-Coding.
143            """
144            __slots__ = ('__object',)
145            def __init__(self, value):
146                self.__object = value
147
148            def __repr__(self):
149                return "<KVC accessor for %r>"%(self.__object,)
150
151            def __getattr__(self, key):
152                try:
153                    return self.__object.valueForKey_(key)
154                except KeyError, msg:
155                    if hasattr(msg, '_pyobjc_info_') and msg._pyobjc_info_['name'] == 'NSUnknownKeyException':
156                        raise AttributeError(key)
157
158                    raise
159            def __setattr__(self, key, value):
160                if not key.startswith('_'):
161                    return self.__object.setValue_forKey_(value, key)
162                else:
163                    super(kvc, self).__setattr__(key, value)
164
165            def __getitem__(self, key):
166                if not isinstance(key, (str, unicode)):
167                    raise TypeError("Key must be string")
168                return self.__object.valueForKey_(key)
169
170            def __setitem__(self, key, value):
171                if not isinstance(key, (str, unicode)):
172                    raise TypeError("Key must be string")
173                return self.__object.setValue_forKey_(value, key)
174
175        type_dict['_'] = property(kvc)
176
177setClassExtender(add_convenience_methods)
178
179
180#
181# The following conveniences should strictly speaking be in
182# in pyobjc-framework-Foundation, but as they are very fundamental
183# we're keeping them here.
184#
185
186def __getitem__objectForKey_(self, key):
187    res = self.objectForKey_(container_wrap(key))
188    return container_unwrap(res, KeyError, key)
189
190def has_key_objectForKey_(self, key):
191    res = self.objectForKey_(container_wrap(key))
192    return res is not None
193
194def get_objectForKey_(self, key, dflt=None):
195    res = self.objectForKey_(container_wrap(key))
196    if res is None:
197        res = dflt
198    return res
199
200CONVENIENCE_METHODS['objectForKey:'] = (
201    ('__getitem__', __getitem__objectForKey_),
202    ('has_key', has_key_objectForKey_),
203    ('get', get_objectForKey_),
204    ('__contains__', has_key_objectForKey_),
205)
206
207def __delitem__removeObjectForKey_(self, key):
208    self.removeObjectForKey_(container_wrap(key))
209
210CONVENIENCE_METHODS['removeObjectForKey:'] = (
211    ('__delitem__', __delitem__removeObjectForKey_),
212)
213
214def update_setObject_forKey_(self, other):
215    # XXX - should this be more flexible?
216    for key, value in other.items():
217        self[key] = value
218
219def setdefault_setObject_forKey_(self, key, dflt=None):
220    try:
221        return self[key]
222    except KeyError:
223        self[key] = dflt
224        return dflt
225
226def __setitem__setObject_forKey_(self, key, value):
227    self.setObject_forKey_(container_wrap(value), container_wrap(key))
228
229def pop_setObject_forKey_(self, key, dflt=None):
230    try:
231        res = self[key]
232    except KeyError:
233        res = dflt
234    else:
235        del self[key]
236    return res
237
238def popitem_setObject_forKey_(self):
239    try:
240        k = self[iter(self).next()]
241    except StopIteration:
242        raise KeyError, "popitem on an empty %s" % (type(self).__name__,)
243    else:
244        return (k, self[k])
245
246CONVENIENCE_METHODS['setObject:forKey:'] = (
247    ('__setitem__', __setitem__setObject_forKey_),
248    ('update', update_setObject_forKey_),
249    ('setdefault', setdefault_setObject_forKey_),
250    ('pop', pop_setObject_forKey_),
251    ('popitem', popitem_setObject_forKey_),
252)
253
254
255CONVENIENCE_METHODS['count'] = (
256    ('__len__', lambda self: self.count()),
257)
258
259CONVENIENCE_METHODS['containsObject:'] = (
260    ('__contains__', lambda self, elem: bool(self.containsObject_(container_wrap(elem)))),
261)
262
263
264
265def objc_hash(self, _max=sys.maxint, _const=((sys.maxint + 1L) * 2L)):
266    rval = self.hash()
267    if rval > _max:
268        rval -= _const
269        # -1 is not a valid hash in Python and hash(x) will
270        # translate a hash of -1 to -2, so we might as well
271        # do it here so that it's not too surprising..
272        if rval == -1:
273            rval = -2
274    return int(rval)
275CONVENIENCE_METHODS['hash'] = (
276    ('__hash__', objc_hash),
277)
278
279CONVENIENCE_METHODS['isEqualTo:'] = (
280    ('__eq__', lambda self, other: bool(self.isEqualTo_(other))),
281)
282
283CONVENIENCE_METHODS['isEqual:'] = (
284    ('__eq__', lambda self, other: bool(self.isEqual_(other))),
285)
286
287CONVENIENCE_METHODS['isGreaterThan:'] = (
288    ('__gt__', lambda self, other: bool(self.isGreaterThan_(other))),
289)
290
291CONVENIENCE_METHODS['isGreaterThanOrEqualTo:'] = (
292    ('__ge__', lambda self, other: bool(self.isGreaterThanOrEqualTo_(other))),
293)
294
295CONVENIENCE_METHODS['isLessThan:'] = (
296    ('__lt__', lambda self, other: bool(self.isLessThan_(other))),
297)
298
299CONVENIENCE_METHODS['isLessThanOrEqualTo:'] = (
300    ('__le__', lambda self, other: bool(self.isLessThanOrEqualTo_(other))),
301)
302
303CONVENIENCE_METHODS['isNotEqualTo:'] = (
304    ('__ne__', lambda self, other: bool(self.isNotEqualTo_(other))),
305)
306
307CONVENIENCE_METHODS['length'] = (
308    ('__len__', lambda self: self.length()),
309)
310
311CONVENIENCE_METHODS['addObject:'] = (
312    ('append', lambda self, item: self.addObject_(container_wrap(item))),
313)
314
315def reverse_exchangeObjectAtIndex_withObjectAtIndex_(self):
316    begin = 0
317    end = len(self) - 1
318    while begin < end:
319        self.exchangeObjectAtIndex_withObjectAtIndex_(begin, end)
320        begin += 1
321        end -= 1
322
323CONVENIENCE_METHODS['exchangeObjectAtIndex:withObjectAtIndex:'] = (
324    ('reverse', reverse_exchangeObjectAtIndex_withObjectAtIndex_),
325)
326
327def ensureArray(anArray):
328    if not isinstance(anArray, (NSArray, list, tuple)):
329        anArray = list(anArray)
330    return anArray
331
332
333def extend_addObjectsFromArray_(self, anArray):
334    self.addObjectsFromArray_(ensureArray(anArray))
335
336CONVENIENCE_METHODS['addObjectsFromArray:'] = (
337    ('extend', extend_addObjectsFromArray_),
338)
339
340def index_indexOfObject_(self, item):
341    from Foundation import NSNotFound
342    res = self.indexOfObject_(container_wrap(item))
343    if res == NSNotFound:
344        raise ValueError, "%s.index(x): x not in list" % (type(self).__name__,)
345    return res
346
347CONVENIENCE_METHODS['indexOfObject:'] = (
348    ('index', index_indexOfObject_),
349)
350
351def insert_insertObject_atIndex_(self, idx, item):
352    if idx < 0:
353        idx += len(self)
354        if idx < 0:
355            raise IndexError("list index out of range")
356    self.insertObject_atIndex_(container_wrap(item), idx)
357
358CONVENIENCE_METHODS['insertObject:atIndex:'] = (
359    ( 'insert', insert_insertObject_atIndex_),
360)
361
362def __getitem__objectAtIndex_(self, idx):
363    if isinstance(idx, slice):
364        start, stop, step = idx.indices(len(self))
365        #if step == 1:
366        #    m = getattr(self, 'subarrayWithRange_', None)
367        #    if m is not None:
368        #        return m((start, stop - start))
369        return [self[i] for i in xrange(start, stop, step)]
370    if idx < 0:
371        idx += len(self)
372        if idx < 0:
373            raise IndexError("list index out of range")
374
375    return container_unwrap(self.objectAtIndex_(idx), RuntimeError)
376
377CONVENIENCE_METHODS['objectAtIndex:'] = (
378    ('__getitem__', __getitem__objectAtIndex_),
379)
380
381def __delitem__removeObjectAtIndex_(self, idx):
382    if isinstance(idx, slice):
383        start, stop, step = idx.indices(len(self))
384        if step == 1:
385            if start > stop:
386                start, stop = stop, start
387            m = getattr(self, 'removeObjectsInRange_', None)
388            if m is not None:
389                m((start, stop - start))
390                return
391        r = range(start, stop, step)
392        r.sort()
393        r.reverse()
394        for i in r:
395            self.removeObjectAtIndex_(i)
396        return
397    if idx < 0:
398        idx += len(self)
399        if idx < 0:
400            raise IndexError("list index out of range")
401
402    self.removeObjectAtIndex_(idx)
403
404def pop_removeObjectAtIndex_(self, idx=-1):
405    length = len(self)
406    if length <= 0:
407        raise IndexError("pop from empty list")
408    elif idx >= length or (idx + length) < 0:
409        raise IndexError("pop index out of range")
410    elif idx < 0:
411        idx += len(self)
412        if idx < 0:
413            raise IndexError("list index out of range")
414    rval = self[idx]
415    self.removeObjectAtIndex_(idx)
416    return rval
417
418def remove_removeObjectAtIndex_(self, obj):
419    idx = self.index(obj)
420    self.removeObjectAtIndex_(idx)
421
422CONVENIENCE_METHODS['removeObjectAtIndex:'] = (
423    ('remove', remove_removeObjectAtIndex_),
424    ('pop', pop_removeObjectAtIndex_),
425    ('__delitem__', __delitem__removeObjectAtIndex_),
426)
427
428def __setitem__replaceObjectAtIndex_withObject_(self, idx, anObject):
429    if isinstance(idx, slice):
430        start, stop, step = idx.indices(len(self))
431        if step == 1:
432            m = getattr(self, 'replaceObjectsInRange_withObjectsFromArray_', None)
433            if m is not None:
434                m((start, stop - start), ensureArray(anObject))
435                return
436        # XXX - implement this..
437        raise NotImplementedError
438    if idx < 0:
439        idx += len(self)
440        if idx < 0:
441            raise IndexError("list index out of range")
442
443    self.replaceObjectAtIndex_withObject_(idx, anObject)
444
445CONVENIENCE_METHODS['replaceObjectAtIndex:withObject:'] = (
446    ('__setitem__', __setitem__replaceObjectAtIndex_withObject_),
447)
448
449def enumeratorGenerator(anEnumerator):
450    while True:
451        yield container_unwrap(anEnumerator.nextObject(), StopIteration)
452
453def dictItems(aDict):
454    """
455    NSDictionary.items()
456    """
457    keys = aDict.allKeys()
458    return zip(keys, imap(aDict.__getitem__, keys))
459
460CONVENIENCE_METHODS['allKeys'] = (
461    ('keys', lambda self: self.allKeys()),
462    ('items', lambda self: dictItems(self)),
463)
464
465CONVENIENCE_METHODS['allValues'] = (
466    ('values', lambda self: self.allValues()),
467)
468
469def itemsGenerator(aDict):
470    for key in aDict:
471        yield (key, aDict[key])
472
473def __iter__objectEnumerator_keyEnumerator(self):
474    meth = getattr(self, 'keyEnumerator', None)
475    if meth is None:
476        meth = self.objectEnumerator
477    return iter(meth())
478
479CONVENIENCE_METHODS['keyEnumerator'] = (
480    ('__iter__', __iter__objectEnumerator_keyEnumerator),
481    ('iterkeys', lambda self: iter(self.keyEnumerator())),
482    ('iteritems', lambda self: itemsGenerator(self)),
483)
484
485CONVENIENCE_METHODS['objectEnumerator'] = (
486    ('__iter__', __iter__objectEnumerator_keyEnumerator),
487    ('itervalues', lambda self: iter(self.objectEnumerator())),
488)
489
490CONVENIENCE_METHODS['reverseObjectEnumerator'] = (
491    ('__reversed__', lambda self: iter(self.reverseObjectEnumerator())),
492)
493
494CONVENIENCE_METHODS['removeAllObjects'] = (
495    ('clear', lambda self: self.removeAllObjects()),
496)
497
498CONVENIENCE_METHODS['dictionaryWithDictionary:'] = (
499    ('copy', lambda self: type(self).dictionaryWithDictionary_(self)),
500)
501
502CONVENIENCE_METHODS['nextObject'] = (
503    ('__iter__', enumeratorGenerator),
504)
505
506#
507# NSNumber seems to be and abstract base-class that is implemented using
508# NSCFNumber, a CoreFoundation 'proxy'.
509#
510NSNull = lookUpClass('NSNull')
511NSArray = lookUpClass('NSArray')
512#null = NSNull.null()
513
514number_wrap = repythonify
515
516def container_wrap(v):
517    if v is None:
518        return NSNull.null()
519    return v
520
521def container_unwrap(v, exc_type, *exc_args):
522    if v is None:
523        raise exc_type(*exc_args)
524    elif v is NSNull.null():
525        return None
526    return v
527
528
529def fromkeys_dictionaryWithObjects_forKeys_(cls, keys, values=None):
530    if not isinstance(keys, (list, tuple)):
531        keys = list(keys)
532    if values is None:
533        values = (None,) * len(keys)
534    elif not isinstance(values, (list, tuple)):
535        values = list(values)
536    return cls.dictionaryWithObjects_forKeys_(values, keys)
537
538CONVENIENCE_METHODS['dictionaryWithObjects:forKeys:'] = (
539    ('fromkeys',
540        classmethod(fromkeys_dictionaryWithObjects_forKeys_)),
541)
542
543
544def sort(self, key=None, reverse=False, cmpfunc=cmp):
545    # NOTE: cmpfunc argument is for backward compatibility.
546    if key is None:
547        if reverse:
548            def doCmp(a, b, cmpfunc):
549                return -cmpfunc(a, b)
550        else:
551            def doCmp(a, b, cmpfunc):
552                return cmpfunc(a, b)
553    else:
554        # This is (a lot) slower than the algoritm used for
555        # list.sort, but so be it.
556        if reverse:
557            def doCmp(a, b, cmpfunc):
558                return -cmpfunc(key(a), key(b))
559        else:
560            def doCmp(a, b, cmpfunc):
561                return cmpfunc(key(a), key(b))
562
563    self.sortUsingFunction_context_(doCmp, cmpfunc)
564
565
566
567CONVENIENCE_METHODS['sortUsingFunction:context:'] = (
568    ('sort', sort),
569)
570
571CONVENIENCE_METHODS['hasPrefix:'] = (
572    ('startswith', lambda self, pfx: self.hasPrefix_(pfx)),
573)
574
575CONVENIENCE_METHODS['hasSuffix:'] = (
576    ('endswith', lambda self, pfx: self.hasSuffix_(pfx)),
577)
578
579
580CONVENIENCE_METHODS['copyWithZone:'] = (
581    ('__copy__', lambda self: self.copyWithZone_(None)),
582)
583
584# This won't work:
585#NSKeyedArchiver = lookUpClass('NSKeyedArchiver')
586#NSKeyedUnarchiver = lookUpClass('NSKeyedUnarchiver')
587#def coder_deepcopy(self, memo):
588#   buf = NSKeyedArchiver.archivedDataWithRootObject_(self)
589#   result = NSKeyedUnarchiver.unarchiveObjectWithData_(buf)
590#   return result
591#
592#CONVENIENCE_METHODS['encodeWithCoder:'] = (
593#   ('__deepcopy__', coder_deepcopy ),
594#)
595
596CLASS_METHODS['NSNull'] = (
597    ('__nonzero__',  lambda self: False ),
598)
599
600NSDecimalNumber = lookUpClass('NSDecimalNumber')
601def _makeD(v):
602    if isinstance(v, NSDecimalNumber):
603        return v
604    return NSDecimalNumber.decimalNumberWithDecimal_(v)
605
606CLASS_METHODS['NSDecimalNumber'] = (
607    ('__add__', lambda self, other: _makeD(self.decimalValue() + other)),
608    ('__radd__', lambda self, other: _makeD(other + self.decimalValue())),
609    ('__sub__', lambda self, other: _makeD(self.decimalValue() - other)),
610    ('__rsub__', lambda self, other: _makeD(other - self.decimalValue())),
611    ('__mul__', lambda self, other: _makeD(self.decimalValue() * other)),
612    ('__rmul__', lambda self, other: _makeD(other * self.decimalValue())),
613    ('__div__', lambda self, other: _makeD(self.decimalValue() / other)),
614    ('__rdiv__', lambda self, other: _makeD(other / self.decimalValue())),
615    ('__mod__', lambda self, other: _makeD(self.decimalValue() % other)),
616    ('__rmod__', lambda self, other: _makeD(other % self.decimalValue())),
617    ('__neg__', lambda self: _makeD(-(self.decimalValue()))),
618    ('__pos__', lambda self: _makeD(+(self.decimalValue()))),
619    ('__abs__', lambda self: _makeD(abs(self.decimalValue()))),
620)
621
622def NSData__getslice__(self, i, j):
623    return self.bytes()[i:j]
624
625def NSData__getitem__(self, item):
626    buff = self.bytes()
627    try:
628        return buff[item]
629    except TypeError:
630        return buff[:][item]
631
632CLASS_METHODS['NSData'] = (
633    ('__str__', lambda self: self.bytes()[:]),
634    ('__getitem__', NSData__getitem__),
635    ('__getslice__', NSData__getslice__),
636)
637
638def NSMutableData__setslice__(self, i, j, sequence):
639    # XXX - could use replaceBytes:inRange:, etc.
640    self.mutableBytes()[i:j] = sequence
641
642def NSMutableData__setitem__(self, item, value):
643    self.mutableBytes()[item] = value
644
645CLASS_METHODS['NSMutableData'] = (
646    ('__setslice__', NSMutableData__setslice__),
647    ('__setitem__', NSMutableData__setitem__),
648)
649
650
651def __call__(self, *args, **kwds):
652    return _block_call(self, self.__block_signature__, args, kwds)
653
654CLASS_METHODS['NSBlock'] = (
655    ('__call__', __call__),
656)
657