447 lines
14 KiB
Python
447 lines
14 KiB
Python
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 <NSString>' 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)
|