import operator from .api import ( NSArray, NSDictionary, NSMutableArray, NSMutableDictionary, NSString, ObjCInstance, for_objcclass, ns_from_py, py_from_ns, ) from .runtime import objc_id, send_message from .types import NSNotFound, NSRange, NSUInteger, unichar # All NSComparisonResult values. NSOrderedAscending = -1 NSOrderedSame = 0 NSOrderedDescending = 1 # Some useful NSStringCompareOptions. NSLiteralSearch = 2 NSBackwardsSearch = 4 @for_objcclass(NSString) class ObjCStrInstance(ObjCInstance): """Provides Pythonic operations on NSString objects that mimic those of Python's str. Note that str objects consist of Unicode code points, whereas NSString objects consist of UTF-16 code units. These are not equivalent for code points greater than U+FFFF. For performance and simplicity, ObjCStrInstance objects behave as sequences of UTF-16 code units, like NSString. (Individual UTF-16 code units are represented as Python str objects of length 1.) If you need to access or iterate over code points instead of UTF-16 code units, use str(nsstring) to convert the NSString to a Python str first. """ def __str__(self): return self.UTF8String.decode("utf-8") def __fspath__(self): return self.__str__() def __eq__(self, other): if isinstance(other, str): return self.isEqualToString(ns_from_py(other)) elif isinstance(other, NSString): return self.isEqualToString(other) else: return super().__eq__(other) def __ne__(self, other): return not self.__eq__(other) # Note: We cannot define a __hash__ for NSString objects; doing so would violate the Python convention that # mutable objects should not be hashable. Although we could disallow hashing for NSMutableString objects, this # would make some immutable strings unhashable as well, because immutable strings can have a runtime class that # is a subclass of NSMutableString. This is not just a theoretical possibility - for example, on OS X 10.11, # isinstance(NSString.string(), NSMutableString) is true. def _compare(self, other, want): """Helper method used to implement the comparison operators. If other is a str or NSString, it is compared to self, and True or False is returned depending on whether the result is one of the wanted values. If other is not a string, NotImplemented is returned. """ if isinstance(other, str): ns_other = ns_from_py(other) elif isinstance(other, NSString): ns_other = other else: return NotImplemented return self.compare(ns_other, options=NSLiteralSearch) in want def __lt__(self, other): return self._compare(other, {NSOrderedAscending}) def __le__(self, other): return self._compare(other, {NSOrderedAscending, NSOrderedSame}) def __ge__(self, other): return self._compare(other, {NSOrderedSame, NSOrderedDescending}) def __gt__(self, other): return self._compare(other, {NSOrderedDescending}) def __contains__(self, value): if not isinstance(value, (str, NSString)): raise TypeError( "'in ' requires str or NSString as left operand, " f"not {type(value).__module__}.{type(value).__qualname__}" ) return self.find(value) != -1 def __len__(self): return self.length def __getitem__(self, key): if isinstance(key, slice): start, stop, step = key.indices(len(self)) if step == 1: return self.substringWithRange(NSRange(start, stop - start)) else: rng = range(start, stop, step) chars = (unichar * len(rng))() for chars_i, self_i in enumerate(rng): chars[chars_i] = ord(self[self_i]) return NSString.stringWithCharacters(chars, length=len(chars)) else: if key < 0: index = len(self) + key else: index = key if index not in range(len(self)): raise IndexError(f"{type(self).__name__} index out of range") return chr(self.characterAtIndex(index)) def __add__(self, other): if isinstance(other, (str, NSString)): return self.stringByAppendingString(other) else: return NotImplemented def __radd__(self, other): if isinstance(other, (str, NSString)): return ns_from_py(other).stringByAppendingString(self) else: return NotImplemented def __mul__(self, other): try: count = operator.index(other) except AttributeError: return NotImplemented if count <= 0: return ns_from_py("") else: # https://stackoverflow.com/a/4608137 return self.stringByPaddingToLength( count * len(self), withString=self, startingAtIndex=0, ) def __rmul__(self, other): return self.__mul__(other) def _find(self, sub, start=None, end=None, *, reverse): if not isinstance(sub, (str, NSString)): raise TypeError( f"must be str or NSString, not {type(sub).__module__}.{type(sub).__qualname__}" ) start, end, _ = slice(start, end).indices(len(self)) if not sub: # Special case: Python considers the empty string to be contained in every string, # at the earliest position searched. NSString considers the empty string to *not* be # contained in any string. This difference is handled here. return end if reverse else start options = NSLiteralSearch if reverse: options |= NSBackwardsSearch found_range = self.rangeOfString( sub, options=options, range=NSRange(start, end - start) ) if found_range.location == NSNotFound: return -1 else: return found_range.location def _index(self, sub, start=None, end=None, *, reverse): found = self._find(sub, start, end, reverse=reverse) if found == -1: raise ValueError("substring not found") else: return found def find(self, sub, start=None, end=None): return self._find(sub, start=start, end=end, reverse=False) def index(self, sub, start=None, end=None): return self._index(sub, start=start, end=end, reverse=False) def rfind(self, sub, start=None, end=None): return self._find(sub, start=start, end=end, reverse=True) def rindex(self, sub, start=None, end=None): return self._index(sub, start=start, end=end, reverse=True) # A fallback method; get the locally defined attribute if it exists; # otherwise, get the attribute from the Python-converted version # of the string def __getattr__(self, attr): try: return super().__getattr__(attr) except AttributeError: return getattr(self.__str__(), attr) @for_objcclass(NSArray) class ObjCListInstance(ObjCInstance): def __getitem__(self, item): if isinstance(item, slice): start, stop, step = item.indices(len(self)) if step == 1: return self.subarrayWithRange(NSRange(start, stop - start)) else: return ns_from_py( [self.objectAtIndex(x) for x in range(start, stop, step)] ) else: if item < 0: index = len(self) + item else: index = item if index not in range(len(self)): raise IndexError(f"{type(self).__name__} index out of range") return self.objectAtIndex(index) def __len__(self): return send_message(self.ptr, "count", restype=NSUInteger, argtypes=[]) def __iter__(self): for i in range(len(self)): yield self.objectAtIndex(i) def __contains__(self, item): return self.containsObject_(item) def __eq__(self, other): return list(self) == other def __ne__(self, other): return not self.__eq__(other) def index(self, value): idx = self.indexOfObject_(value) if idx == NSNotFound: raise ValueError(f"{value!r} is not in list") return idx def count(self, value): return len([x for x in self if x == value]) def copy(self): return ObjCInstance(send_message(self, "copy", restype=objc_id, argtypes=[])) @for_objcclass(NSMutableArray) class ObjCMutableListInstance(ObjCListInstance): def __setitem__(self, item, value): if isinstance(item, slice): arr = ns_from_py(value) if not isinstance(arr, NSArray): raise TypeError( f"{type(value).__module__}.{type(value).__qualname__} " "is not convertible to NSArray" ) start, stop, step = item.indices(len(self)) if step == 1: self.replaceObjectsInRange( NSRange(start, stop - start), withObjectsFromArray=arr ) else: indices = range(start, stop, step) if len(arr) != len(indices): raise ValueError( f"attempt to assign sequence of size {len(value)} " f"to extended slice of size {len(indices)}" ) for idx, obj in zip(indices, arr): self.replaceObjectAtIndex(idx, withObject=obj) else: if item < 0: index = len(self) + item else: index = item if index not in range(len(self)): raise IndexError(f"{type(self).__name__} assignment index out of range") self.replaceObjectAtIndex(index, withObject=value) def __delitem__(self, item): if isinstance(item, slice): start, stop, step = item.indices(len(self)) if step == 1: self.removeObjectsInRange(NSRange(start, stop - start)) else: for idx in sorted(range(start, stop, step), reverse=True): self.removeObjectAtIndex(idx) else: if item < 0: index = len(self) + item else: index = item if index not in range(len(self)): raise IndexError(f"{type(self).__name__} assignment index out of range") self.removeObjectAtIndex_(index) def copy(self): return self.mutableCopy() def append(self, value): self.addObject_(value) def extend(self, values): for value in values: self.addObject_(value) def clear(self): self.removeAllObjects() def pop(self, item=-1): value = self[item] del self[item] return value def remove(self, value): del self[self.index(value)] def reverse(self): self.setArray(self.reverseObjectEnumerator().allObjects()) def insert(self, idx, value): self.insertObject_atIndex_(value, idx) @for_objcclass(NSDictionary) class ObjCDictInstance(ObjCInstance): def __getitem__(self, item): v = self.objectForKey_(item) if v is None: raise KeyError(item) return v def __len__(self): return self.count def __iter__(self): yield from self.allKeys() def __contains__(self, item): return self.objectForKey_(item) is not None def __eq__(self, other): return py_from_ns(self) == other def __ne__(self, other): return not self.__eq__(other) def get(self, item, default=None): v = self.objectForKey_(item) if v is None: return default return v def keys(self): return self.allKeys() def values(self): return self.allValues() def items(self): for key in self.allKeys(): yield key, self.objectForKey_(key) def copy(self): return ObjCInstance(send_message(self, "copy", restype=objc_id, argtypes=[])) @for_objcclass(NSMutableDictionary) class ObjCMutableDictInstance(ObjCDictInstance): no_pop_default = object() def __setitem__(self, item, value): self.setObject_forKey_(value, item) def __delitem__(self, item): if item not in self: raise KeyError(item) self.removeObjectForKey_(item) def copy(self): return self.mutableCopy() def clear(self): self.removeAllObjects() def pop(self, item, default=no_pop_default): if item not in self: if default is not self.no_pop_default: return default else: raise KeyError(item) value = self.objectForKey_(item) self.removeObjectForKey_(item) return value def popitem(self): if len(self) == 0: raise KeyError(f"popitem(): {type(self).__name__} is empty") key = self.allKeys().firstObject() value = self.objectForKey_(key) self.removeObjectForKey_(key) return key, value def setdefault(self, key, default=None): value = self.objectForKey_(key) if value is None: value = default if value is not None: self.setObject_forKey_(default, key) return value def update(self, new=None, **kwargs): if new is not None: kwargs.update(new) for k, v in kwargs.items(): self.setObject_forKey_(v, k)