From ff08af1b1de41fd9949b3b4e554743a25dea5990 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Mon, 26 Feb 2018 14:26:08 +0100 Subject: [PATCH 01/30] Move test structs to top level so they can be reused more easily --- tests/test_core.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 51fabf4d..5e390a5e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -33,6 +33,18 @@ faulthandler.enable() +class struct_int_sized(Structure): + _fields_ = [("x", c_char * 4)] + + +class struct_oddly_sized(Structure): + _fields_ = [("x", c_char * 5)] + + +class struct_large(Structure): + _fields_ = [("x", c_char * 17)] + + class RubiconTest(unittest.TestCase): def test_sel_by_name(self): self.assertEqual(SEL(b"foobar").name, b"foobar") @@ -663,21 +675,12 @@ def test_struct_return(self): Example = ObjCClass('Example') example = Example.alloc().init() - class struct_int_sized(Structure): - _fields_ = [("x", c_char * 4)] types.register_encoding(b'{int_sized=[4c]}', struct_int_sized) - self.assertEqual(example.intSizedStruct().x, b"abc") - class struct_oddly_sized(Structure): - _fields_ = [("x", c_char * 5)] - types.register_encoding(b'{oddly_sized=[5c]}', struct_oddly_sized) self.assertEqual(example.oddlySizedStruct().x, b"abcd") - class struct_large(Structure): - _fields_ = [("x", c_char * 17)] - types.register_encoding(b'{large=[17c]}', struct_large) self.assertEqual(example.largeStruct().x, b"abcdefghijklmnop") @@ -686,19 +689,8 @@ def test_struct_return_send(self): Example = ObjCClass('Example') example = Example.alloc().init() - class struct_int_sized(Structure): - _fields_ = [("x", c_char * 4)] - self.assertEqual(send_message(example, "intSizedStruct", restype=struct_int_sized).x, b"abc") - - class struct_oddly_sized(Structure): - _fields_ = [("x", c_char * 5)] - self.assertEqual(send_message(example, "oddlySizedStruct", restype=struct_oddly_sized).x, b"abcd") - - class struct_large(Structure): - _fields_ = [("x", c_char * 17)] - self.assertEqual(send_message(example, "largeStruct", restype=struct_large).x, b"abcdefghijklmnop") def test_object_return(self): From f1c6c714371fd6cd172ed83bffffc812e2fd10cd Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 27 Feb 2018 01:39:31 +0100 Subject: [PATCH 02/30] Reimplement ivar functions so they work properly for non-objects The runtime's ivar get/set functions must be used for weak object pointers, but may not work correctly for non-pointer-sized types. (See comments for details.) Our ivar get/set functions now use the runtime functions for object pointers, and manual memory access for all other types. The functions have been given shorter names. The old functions were never public and have a different signature, so there's no reason to keep the old names. Also added a unit test for ivars of various types. --- rubicon/objc/__init__.py | 4 +- rubicon/objc/runtime.py | 103 +++++++++++++++++++++++++++------------ tests/test_core.py | 30 +++++++++++- 3 files changed, 101 insertions(+), 36 deletions(-) diff --git a/rubicon/objc/__init__.py b/rubicon/objc/__init__.py index 06f3180f..060f7d9c 100644 --- a/rubicon/objc/__init__.py +++ b/rubicon/objc/__init__.py @@ -13,9 +13,9 @@ from .runtime import ( # noqa: F401 IMP, SEL, Block, Class, Ivar, Method, NSArray, NSDictionary, NSMutableArray, NSMutableDictionary, NSObject, - NSObjectProtocol, ObjCBlock, ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, ns_from_py, + NSObjectProtocol, ObjCBlock, ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, get_ivar, ns_from_py, objc_classmethod, objc_const, objc_id, objc_ivar, objc_method, objc_property, objc_property_t, objc_rawmethod, - py_from_ns, send_message, send_super, + py_from_ns, send_message, send_super, set_ivar, ) from .types import ( # noqa: F401 CFIndex, CFRange, CGFloat, CGGlyph, CGPoint, CGPointMake, CGRect, diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index de1cf792..bfbb031d 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -7,13 +7,13 @@ CDLL, CFUNCTYPE, POINTER, ArgumentError, Array, Structure, Union, addressof, alignment, byref, c_bool, c_char_p, c_double, c_float, c_int, c_int32, c_int64, c_longdouble, c_size_t, c_uint, c_uint8, c_ulong, - c_void_p, cast, sizeof, string_at, util, + c_void_p, cast, memmove, sizeof, string_at, util, ) from . import ctypes_patch from .types import ( __arm__, __i386__, __x86_64__, compound_value_for_sequence, - ctype_for_type, ctypes_for_method_encoding, encoding_for_ctype, + ctype_for_encoding, ctype_for_type, ctypes_for_method_encoding, encoding_for_ctype, register_ctype_for_type, with_encoding, with_preferred_encoding, ) @@ -54,7 +54,7 @@ 'encoding_from_annotation', 'for_objcclass', 'get_class', - 'get_instance_variable', + 'get_ivar', 'get_metaclass', 'get_superclass_of_object', 'get_type_for_objcclass_map', @@ -77,7 +77,7 @@ 'register_type_for_objcclass', 'send_message', 'send_super', - 'set_instance_variable', + 'set_ivar', 'should_use_fpret', 'should_use_stret', 'type_for_objcclass', @@ -425,18 +425,15 @@ def object_isClass(obj): libobjc.object_getClassName.restype = c_char_p libobjc.object_getClassName.argtypes = [objc_id] -# Ivar object_getInstanceVariable(id obj, const char *name, void **outValue) -libobjc.object_getInstanceVariable.restype = Ivar -libobjc.object_getInstanceVariable.argtypes = [objc_id, c_char_p, POINTER(c_void_p)] +# Note: The following functions only work for exactly pointer-sized ivars. +# To use non-pointer-sized ivars reliably, the memory location must be calculated manually (using ivar_getOffset) +# and then used as a pointer. This "manual" way can be used for all ivars except weak object ivars - these must be +# accessed through the runtime functions in order to work correctly. # id object_getIvar(id object, Ivar ivar) libobjc.object_getIvar.restype = objc_id libobjc.object_getIvar.argtypes = [objc_id, Ivar] -# Ivar object_setInstanceVariable(id obj, const char *name, void *value) -# Set argtypes based on the data type of the instance variable. -libobjc.object_setInstanceVariable.restype = Ivar - # void object_setIvar(id object, Ivar ivar, id value) libobjc.object_setIvar.restype = None libobjc.object_setIvar.argtypes = [objc_id, Ivar, objc_id] @@ -753,17 +750,60 @@ def add_ivar(cls, name, vartype): ) -def set_instance_variable(obj, varname, value, vartype): - "Do the equivalent of `obj.varname = value`, where value is of type vartype." - libobjc.object_setInstanceVariable.argtypes = [objc_id, c_char_p, vartype] - libobjc.object_setInstanceVariable(obj, ensure_bytes(varname), value) +def get_ivar(obj, varname): + """Get the value of obj's ivar named varname. + The returned object is a ctypes data object. + For non-object types (everything except objc_id and subclasses), the returned data object is backed by the ivar's + actual memory. This means that the data object is only usable as long as the "owner" object is alive, and writes + to it will directly change the ivar's value. + For object types, the returned data object is independent of the ivar's memory. This is because object ivars may + be weak, and thus cannot always be accessed directly by their address. + """ -def get_instance_variable(obj, varname, vartype): - "Return the value of `obj.varname`, where the value is of type vartype." - variable = vartype() - libobjc.object_getInstanceVariable(obj, ensure_bytes(varname), byref(variable)) - return variable.value + try: + obj = obj._as_parameter_ + except AttributeError: + pass + + ivar = libobjc.class_getInstanceVariable(libobjc.object_getClass(obj), ensure_bytes(varname)) + vartype = ctype_for_encoding(libobjc.ivar_getTypeEncoding(ivar)) + + if isinstance(vartype, objc_id): + return cast(libobjc.object_getIvar(obj, ivar), vartype) + else: + return vartype.from_address(obj.value + libobjc.ivar_getOffset(ivar)) + + +def set_ivar(obj, varname, value): + """Set obj's ivar varname to value. + + value must be a ctypes data object whose type matches that of the ivar. + """ + + try: + obj = obj._as_parameter_ + except AttributeError: + pass + + ivar = libobjc.class_getInstanceVariable(libobjc.object_getClass(obj), ensure_bytes(varname)) + vartype = ctype_for_encoding(libobjc.ivar_getTypeEncoding(ivar)) + + if not isinstance(value, vartype): + raise TypeError( + "Incompatible type for ivar {!r}: {!r} is not a subclass of the ivar's type {!r}" + .format(varname, type(value), vartype) + ) + elif sizeof(type(value)) != sizeof(vartype): + raise TypeError( + "Incompatible type for ivar {!r}: {!r} has size {}, but the ivar's type {!r} has size {}" + .format(varname, type(value), sizeof(type(value)), vartype, sizeof(vartype)) + ) + + if isinstance(vartype, objc_id): + libobjc.object_setIvar(obj, ivar, value) + else: + memmove(obj.value + libobjc.ivar_getOffset(ivar), addressof(value), sizeof(vartype)) ###################################################################### @@ -1086,8 +1126,7 @@ def protocol_register(proto, attr): class objc_ivar(object): - """Add instance variable named varname to the subclass. - varname should be a string. + """Add an instance variable of type vartype to the subclass. vartype is a ctypes type. The class must be registered AFTER adding instance variables. """ @@ -1927,16 +1966,16 @@ class DeallocationObserver(NSObject): @objc_rawmethod def initWithObject_(self, cmd, anObject): - self = send_super(self, 'init') - self = self.value - set_instance_variable(self, 'observed_object', anObject, objc_id) - return self + self = send_message(self, 'init', restype=objc_id, argtypes=[]) + if self is not None: + set_ivar(self, 'observed_object', anObject) + return self.value @objc_rawmethod def dealloc(self, cmd) -> None: - anObject = get_instance_variable(self, 'observed_object', objc_id) - ObjCInstance._cached_objects.pop(anObject, None) - send_super(self, 'dealloc') + anObject = get_ivar(self, 'observed_object') + ObjCInstance._cached_objects.pop(anObject.value, None) + send_super(self, 'dealloc', restype=None, argtypes=[]) @objc_rawmethod def finalize(self, cmd) -> None: @@ -1944,9 +1983,9 @@ def finalize(self, cmd) -> None: # (which would have to be explicitly started with # objc_startCollectorThread(), so probably not too much reason # to have this here, but I guess it can't hurt.) - anObject = get_instance_variable(self, 'observed_object', objc_id) - ObjCInstance._cached_objects.pop(anObject, None) - send_super(self, 'finalize') + anObject = get_ivar(self, 'observed_object') + ObjCInstance._cached_objects.pop(anObject.value, None) + send_super(self, 'finalize', restype=None, argtypes=[]) def objc_const(dll, name): diff --git a/tests/test_core.py b/tests/test_core.py index 5e390a5e..5896d117 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -12,8 +12,8 @@ from rubicon.objc import ( SEL, NSEdgeInsets, NSEdgeInsetsMake, NSMakeRect, NSObject, NSObjectProtocol, NSRange, NSRect, NSSize, NSUInteger, ObjCClass, - ObjCInstance, ObjCMetaClass, ObjCProtocol, at, objc_classmethod, - objc_const, objc_method, objc_property, send_message, send_super, types, + ObjCInstance, ObjCMetaClass, ObjCProtocol, at, get_ivar, objc_classmethod, + objc_const, objc_id, objc_ivar, objc_method, objc_property, send_message, send_super, set_ivar, types, ) from rubicon.objc.runtime import ObjCBoundMethod, libobjc @@ -836,6 +836,32 @@ def test_no_duplicate_protocols(self): class DuplicateProtocol(NSObject, protocols=[NSObjectProtocol, NSObjectProtocol]): pass + def test_class_ivars(self): + """An Objective-C class can have instance variables.""" + + class Ivars(NSObject): + object = objc_ivar(objc_id) + int = objc_ivar(c_int) + rect = objc_ivar(NSRect) + + ivars = Ivars.alloc().init() + + set_ivar(ivars, 'object', at('foo').ptr) + set_ivar(ivars, 'int', c_int(12345)) + set_ivar(ivars, 'rect', NSMakeRect(12, 34, 56, 78)) + + s = ObjCInstance(get_ivar(ivars, 'object')) + self.assertEqual(str(s), 'foo') + + i = get_ivar(ivars, 'int') + self.assertEqual(i.value, 12345) + + r = get_ivar(ivars, 'rect') + self.assertEqual(r.origin.x, 12) + self.assertEqual(r.origin.y, 34) + self.assertEqual(r.size.width, 56) + self.assertEqual(r.size.height, 78) + def test_class_properties(self): "A Python class can have ObjC properties with synthesized getters and setters." From 0cdf1bb9cf0d7fc01c96b45539901a40194709d3 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 27 Feb 2018 14:21:48 +0100 Subject: [PATCH 03/30] Implement properties based on ivars instead of Python attributes Python attributes are not reliable for storing property values, see PR #95. By using ivars there can be no possible conflicts with Python attributes. This also adds support for properties of types other than objc_id. --- rubicon/objc/runtime.py | 93 ++++++++++++++++++++++++----------------- tests/test_core.py | 28 +++++++++++-- 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index bfbb031d..912f3b9d 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -1141,52 +1141,67 @@ def protocol_register(self, proto, attr): class objc_property(object): - def __init__(self): - pass + """Add a property to an Objective-C class. - def register(self, cls, attr): - def getter(_self) -> ObjCInstance: - return getattr(_self, '_' + attr, None) - - def setter(_self, new): - if not hasattr(_self, '_' + attr): - setattr(_self, '_' + attr, None) - if getattr(_self, '_' + attr) is None: - setattr(_self, '_' + attr, new) - if new is not None: - new.retain() - else: - getattr(_self, '_' + attr).autorelease() - setattr(_self, '_' + attr, new) - if new is not None: - getattr(_self, '_' + attr).retain() - - getter_encoding = encoding_from_annotation(getter) - setter_encoding = encoding_from_annotation(setter) - - def _objc_getter(objc_self, objc_cmd): - py_self = ObjCInstance(objc_self) - result = ns_from_py(getter(py_self)) - if result is None: - return None - else: - return result.ptr.value + An ivar, a getter and a setter are automatically generated. + If the property's type is objc_id or a subclass, the generated setter keeps the stored object retained, and + releases it when it is replaced. + """ + + def __init__(self, vartype=objc_id): + super().__init__() - def _objc_setter(objc_self, objc_cmd, name): - py_self = ObjCInstance(objc_self) - setter(py_self, ObjCInstance(name)) + self.vartype = ctype_for_type(vartype) + + def pre_register(self, ptr, attr): + add_ivar(ptr, '_' + attr, self.vartype) + + def register(self, cls, attr): + def _objc_getter(objc_self, _cmd): + value = get_ivar(objc_self, '_' + attr) + # ctypes complains when a callback returns a "boxed" primitive type, so we have to manually unbox it. + # If the data object has a value attribute and is not a structure or union, assume that it is + # a primitive and unbox it. + if not isinstance(value, (Structure, Union)): + try: + value = value.value + except AttributeError: + pass + + return value + + def _objc_setter(objc_self, _cmd, new_value): + if not isinstance(new_value, self.vartype): + # If vartype is a primitive, then new_value may be unboxed. If that is the case, box it manually. + new_value = self.vartype(new_value) + old_value = get_ivar(objc_self, '_' + attr) + if issubclass(self.vartype, objc_id) and new_value: + # If the new value is a non-null object, retain it. + send_message(new_value, 'retain', restype=objc_id, argtypes=[]) + set_ivar(objc_self, '_' + attr, new_value) + if issubclass(self.vartype, objc_id) and old_value: + # If the old value is a non-null object, release it. + send_message(old_value, 'release', restype=None, argtypes=[]) setter_name = 'set' + attr[0].upper() + attr[1:] + ':' - cls.imp_keep_alive_table[attr] = add_method(cls.ptr, attr, _objc_getter, getter_encoding) - cls.imp_keep_alive_table[setter_name] = add_method(cls.ptr, setter_name, _objc_setter, setter_encoding) + cls.imp_keep_alive_table[attr] = add_method( + cls.ptr, attr, _objc_getter, + [self.vartype, ObjCInstance, SEL], + ) + cls.imp_keep_alive_table[setter_name] = add_method( + cls.ptr, setter_name, _objc_setter, + [None, ObjCInstance, SEL, self.vartype], + ) def protocol_register(self, proto, attr): - attrs = (objc_property_attribute_t * 2)( - objc_property_attribute_t(b'T', b'@'), # Type: id - objc_property_attribute_t(b'&', b''), # retain - ) - libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs, 2, True, True) + attrs = [ + objc_property_attribute_t(b'T', encoding_for_ctype(self.vartype)), # Type: vartype + ] + if issubclass(self.vartype, objc_id): + attrs.append(objc_property_attribute_t(b'&', b'')) # retain + attrs_array = (objc_property_attribute_t * len(attrs))(*attrs) + libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs_array, len(attrs), True, True) def objc_rawmethod(f): diff --git a/tests/test_core.py b/tests/test_core.py index 5896d117..4181d98c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -869,9 +869,8 @@ def test_class_properties(self): class URLBox(NSObject): - # takes no type: All properties are pointers - url = objc_property() - data = objc_property() + url = objc_property(ObjCInstance) + data = objc_property(ObjCInstance) @objc_method def getSchemeIfPresent(self): @@ -914,6 +913,29 @@ def getSchemeIfPresent(self): box.data = None self.assertIsNone(box.data) + def test_class_nonobject_properties(self): + """An Objective-C class can have properties of non-object types.""" + + class Properties(NSObject): + object = objc_property(ObjCInstance) + int = objc_property(c_int) + rect = objc_property(NSRect) + + properties = Properties.alloc().init() + + properties.object = at('foo') + properties.int = 12345 + properties.rect = NSMakeRect(12, 34, 56, 78) + + self.assertEqual(properties.object, 'foo') + self.assertEqual(properties.int, 12345) + + r = properties.rect + self.assertEqual(r.origin.x, 12) + self.assertEqual(r.origin.y, 34) + self.assertEqual(r.size.width, 56) + self.assertEqual(r.size.height, 78) + def test_class_with_wrapped_methods(self): """An ObjCClass can have wrapped methods.""" From e344fa50b878f766336e5c5780897512212268ea Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 27 Feb 2018 14:37:37 +0100 Subject: [PATCH 04/30] Make objc_property add an actual property to the class Previously, for properties on classes, the getter and setter were added, but not declared as a property. --- rubicon/objc/runtime.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index 912f3b9d..6d028d31 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -186,6 +186,13 @@ class objc_property_t(c_void_p): pass +class objc_property_attribute_t(Structure): + _fields_ = [ + ('name', c_char_p), + ('value', c_char_p), + ] + + ###################################################################### # void free(void *) @@ -200,6 +207,11 @@ class objc_property_t(c_void_p): libobjc.class_addMethod.restype = c_bool libobjc.class_addMethod.argtypes = [Class, SEL, IMP, c_char_p] +# BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, +# unsigned int attributeCount) +libobjc.class_addProperty.restype = c_bool +libobjc.class_addProperty.argtypes = [Class, c_char_p, POINTER(objc_property_attribute_t), c_uint] + # BOOL class_addProtocol(Class cls, Protocol *protocol) libobjc.class_addProtocol.restype = c_bool libobjc.class_addProtocol.argtypes = [Class, objc_id] @@ -441,13 +453,6 @@ def object_isClass(obj): ###################################################################### -class objc_property_attribute_t(Structure): - _fields_ = [ - ('name', c_char_p), - ('value', c_char_p), - ] - - # const char *property_getAttributes(objc_property_t property) libobjc.property_getAttributes.restype = c_char_p libobjc.property_getAttributes.argtypes = [objc_property_t] @@ -1153,6 +1158,14 @@ def __init__(self, vartype=objc_id): self.vartype = ctype_for_type(vartype) + def _get_property_attributes(self): + attrs = [ + objc_property_attribute_t(b'T', encoding_for_ctype(self.vartype)), # Type: vartype + ] + if issubclass(self.vartype, objc_id): + attrs.append(objc_property_attribute_t(b'&', b'')) # retain + return (objc_property_attribute_t * len(attrs))(*attrs) + def pre_register(self, ptr, attr): add_ivar(ptr, '_' + attr, self.vartype) @@ -1194,14 +1207,12 @@ def _objc_setter(objc_self, _cmd, new_value): [None, ObjCInstance, SEL, self.vartype], ) + attrs = self._get_property_attributes() + libobjc.class_addProperty(cls, ensure_bytes(attr), attrs, len(attrs)) + def protocol_register(self, proto, attr): - attrs = [ - objc_property_attribute_t(b'T', encoding_for_ctype(self.vartype)), # Type: vartype - ] - if issubclass(self.vartype, objc_id): - attrs.append(objc_property_attribute_t(b'&', b'')) # retain - attrs_array = (objc_property_attribute_t * len(attrs))(*attrs) - libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs_array, len(attrs), True, True) + attrs = self._get_property_attributes() + libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs, len(attrs), True, True) def objc_rawmethod(f): From bba37e7a49ce291b1409fa77cc53ba9780bac970 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 27 Feb 2018 14:53:05 +0100 Subject: [PATCH 05/30] Update changelog --- docs/background/releases.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/background/releases.rst b/docs/background/releases.rst index ee7e90cd..9be782a4 100644 --- a/docs/background/releases.rst +++ b/docs/background/releases.rst @@ -4,6 +4,10 @@ Release History (next version) -------------- +* Added support for ``objc_property``s with non-object types. +* Added public ``get_ivar`` and ``set_ivar`` functions for manipulating ivars. +* Changed the implementation of ``objc_property`` to use ivars instead of Python attributes for storage. This fixes name conflicts in some situations. +* Fixed ``objc_property`` setters on non-macOS platforms. (cculianu) * Fixed various bugs in the collection ``ObjCInstance`` subclasses: * Fixed getting/setting/deleting items or slices with indices lower than ``-len(obj)``. Previously this crashed Python, now an ``IndexError`` is raised. * Fixed slices with step size 0. Previously they were ignored and 1 was incorrectly used as the step size, now an ``IndexError`` is raised. From 4588b1ef1435a89718867a16ff6cb6084236832a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 27 May 2018 16:27:16 +0200 Subject: [PATCH 06/30] Refs #115 - Add more methods on the ObjCString interface. --- rubicon/objc/collections.py | 129 ++++++++++++++++++++-- tests/test_NSString.py | 209 ++++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+), 6 deletions(-) diff --git a/rubicon/objc/collections.py b/rubicon/objc/collections.py index 271a612c..fd4c5f03 100644 --- a/rubicon/objc/collections.py +++ b/rubicon/objc/collections.py @@ -168,12 +168,6 @@ def _find(self, sub, start=None, end=None, *, reverse): else: return found_range.location - def find(self, sub, start=None, end=None): - return self._find(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 _index(self, sub, start=None, end=None, *, reverse): found = self._find(sub, start, end, reverse=reverse) if found == -1: @@ -181,12 +175,135 @@ def _index(self, sub, start=None, end=None, *, reverse): else: return found + def capitalize(self): + return str(self).capitalize() + + def casefold(self): + return str(self).casefold() + + def center(self, width, fillchar=' '): + return str(self).center(width, fillchar) + + def count(self, sub, start=None, end=None): + return str(self).count(sub, start, end) + + def encode(self, encoding='utf-8', errors='strict'): + return str(self).encode(encoding, errors=errors) + + def endswith(self, sub, start=None, end=None): + return str(self).endswith(sub, start, end) + + def expandtabs(self, tabsize=8): + return str(self).expandtabs(tabsize) + + def find(self, sub, start=None, end=None): + return self._find(sub, start=start, end=end, reverse=False) + + def format(self, *args, **kwargs): + return str(self).format(*args, **kwargs) + + def format_map(self, mapping): + return str(self).format_map(mapping) + def index(self, sub, start=None, end=None): return self._index(sub, start=start, end=end, reverse=False) + def isalnum(self): + return str(self).isalnum() + + def isalpha(self): + return str(self).isalpha() + + def isdecimal(self): + return str(self).isdecimal() + + def isdigit(self): + return str(self).isdigit() + + def isidentifier(self): + return str(self).isidentifier() + + def islower(self): + return str(self).islower() + + def isnumeric(self): + return str(self).isnumeric() + + def isprintable(self): + return str(self).isprintable() + + def isspace(self): + return str(self).isspace() + + def istitle(self): + return str(self).istitle() + + def isupper(self): + return str(self).isupper() + + def join(self, iterable): + return str(self).join(iterable) + + def ljust(self, width, fillchar=' '): + return str(self).ljust(width, fillchar) + + def lower(self): + return str(self).lower() + + def lstrip(self, chars=None): + return str(self).lstrip(chars) + + def maketrans(self, x, *args, **kwargs): + return str(self).maketrans(x, *args, **kwargs) + + def partition(self, sep): + return str(self).partition(sep) + + def replace(self, old, new, count=-1): + return str(self).replace(old, new, count) + + 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) + def rjust(self, width, fillchar=' '): + return str(self).rjust(width, fillchar) + + def rpartition(self, sep): + return str(self).rpartition(sep) + + def rsplit(self, sep=None, maxsplit=-1): + return str(self).rsplit(sep=sep, maxsplit=maxsplit) + + def rstrip(self, chars=None): + return str(self).rstrip(chars) + + def split(self, sep=None, maxsplit=-1): + return str(self).split(sep=sep, maxsplit=maxsplit) + + def splitlines(self, keepends=False): + return str(self).splitlines() + + def strip(self, chars=None): + return str(self).strip(chars) + + def swapcase(self): + return str(self).swapcase() + + def title(self): + return str(self).title() + + def translate(self, table): + return str(self).translate(table) + + def upper(self): + return str(self).upper() + + def zfill(self, width): + return str(self).zfill(width) + @for_objcclass(NSArray) class ObjCListInstance(ObjCInstance): diff --git a/tests/test_NSString.py b/tests/test_NSString.py index 812b0adf..c154f8bf 100644 --- a/tests/test_NSString.py +++ b/tests/test_NSString.py @@ -196,3 +196,212 @@ def test_nsstring_mul_rmul(self): ns_repeated = ns_from_py(py_repeated) self.assertEqual(ns_str * n, ns_repeated) self.assertEqual(n * ns_str, ns_repeated) + + def test_nsstring_capitalize(self): + ns_str = ns_from_py('lower, UPPER & Mixed!') + self.assertEqual(ns_str.capitalize(), 'Lower, upper & mixed!') + + def test_nsstring_casefold(self): + ns_str = ns_from_py('lower, UPPER & Mixed!') + self.assertEqual(ns_str.casefold(), 'lower, upper & mixed!') + + def test_nsstring_center(self): + ns_str = ns_from_py('hello') + self.assertEqual(ns_str.center(20), ' hello ') + self.assertEqual(ns_str.center(20, '*'), '*******hello********') + + def test_nsstring_count(self): + ns_str = ns_from_py('hello world') + self.assertEqual(ns_str.count('x'), 0) + self.assertEqual(ns_str.count('l'), 3) + self.assertEqual(ns_str.count('l', start=5), 1) + self.assertEqual(ns_str.count('l', end=8), 2) + self.assertEqual(ns_str.count('l', start=4, end=8), 0) + + def test_nsstring_encode(self): + ns_str = ns_from_py('Uñîçö∂€ string') + self.assertEqual(ns_str.encode('utf-8'), b'U\xc3\xb1\xc3\xae\xc3\xa7\xc3\xb6\xe2\x88\x82\xe2\x82\xac string') + self.assertEqual(ns_str.encode('utf-16'), b'\xff\xfeU\x00\xf1\x00\xee\x00\xe7\x00\xf6\x00\x02"\xac \x00s\x00t\x00r\x00i\x00n\x00g\x00') + + with self.assertRaises(UnicodeEncodeError): + ns_str.encode('ascii') + self.assertEqual(ns_str.encode('ascii', 'ignore'), b'U string') + + def test_nsstring_endswith(self): + ns_str = ns_from_py('Hello world') + self.assertTrue(ns_str.endswith('world')) + self.assertFalse(ns_str.endswith('cake')) + + def test_nsstring_expandtabs(self): + ns_str = ns_from_py('hello\tworld') + self.assertEqual(ns_str.expandtabs(), 'hello world') + self.assertEqual(ns_str.expandtabs(4), 'hello world') + self.assertEqual(ns_str.expandtabs(10), 'hello world') + + def test_nsstring_format(self): + ns_str = ns_from_py('hello {}') + self.assertEqual(ns_str.format('world'), 'hello world') + + def test_nsstring_format_map(self): + ns_str = ns_from_py('hello {name}') + self.assertEqual(ns_str.format_map({'name': 'world'}), 'hello world') + + def test_nsstring_isalnum(self): + self.assertTrue(ns_from_py('abcd').isalnum()) + self.assertTrue(ns_from_py('1234').isalnum()) + self.assertTrue(ns_from_py('abcd1234').isalnum()) + + def test_nsstring_isalpha(self): + self.assertTrue(ns_from_py('abcd').isalpha()) + self.assertFalse(ns_from_py('1234').isalpha()) + self.assertFalse(ns_from_py('abcd1234').isalpha()) + + def test_nsstring_isdecimal(self): + self.assertFalse(ns_from_py('abcd').isdecimal()) + self.assertTrue(ns_from_py('1234').isdecimal()) + self.assertFalse(ns_from_py('abcd1234').isdecimal()) + + def test_nsstring_isdigit(self): + self.assertFalse(ns_from_py('abcd').isdigit()) + self.assertTrue(ns_from_py('1234').isdigit()) + self.assertFalse(ns_from_py('abcd1234').isdigit()) + + def test_nsstring_isidentifier(self): + ns_str = ns_from_py('') + self.assertTrue(ns_from_py('def').isidentifier()) + self.assertTrue(ns_from_py('class').isidentifier()) + self.assertTrue(ns_from_py('hello').isidentifier()) + self.assertFalse(ns_from_py('boo!').isidentifier()) + + def test_nsstring_islower(self): + self.assertTrue(ns_from_py('abcd').islower()) + self.assertFalse(ns_from_py('ABCD').islower()) + self.assertFalse(ns_from_py('1234').islower()) + self.assertTrue(ns_from_py('abcd1234').islower()) + self.assertFalse(ns_from_py('ABCD1234').islower()) + + def test_nsstring_isnumeric(self): + self.assertFalse(ns_from_py('abcd').isdigit()) + self.assertTrue(ns_from_py('1234').isdigit()) + self.assertFalse(ns_from_py('abcd1234').isdigit()) + + def test_nsstring_isprintable(self): + self.assertFalse(ns_from_py('\x09').isprintable()) + self.assertTrue(ns_from_py('Hello').isprintable()) + + def test_nsstring_isspace(self): + self.assertTrue(ns_from_py(' ').isspace()) + self.assertTrue(ns_from_py(' ').isspace()) + self.assertFalse(ns_from_py('Hello world').isspace()) + self.assertFalse(ns_from_py('Hello').isspace()) + + def test_nsstring_istitle(self): + self.assertTrue(ns_from_py('Hello World').istitle()) + self.assertFalse(ns_from_py('hello world').istitle()) + self.assertFalse(ns_from_py('Hello world').istitle()) + self.assertFalse(ns_from_py('Hello WORLD').istitle()) + self.assertFalse(ns_from_py('HELLO WORLD').istitle()) + + def test_nsstring_isupper(self): + self.assertFalse(ns_from_py('abcd').isupper()) + self.assertTrue(ns_from_py('ABCD').isupper()) + self.assertFalse(ns_from_py('1234').isupper()) + self.assertFalse(ns_from_py('abcd1234').isupper()) + self.assertTrue(ns_from_py('ABCD1234').isupper()) + + def test_nsstring_join(self): + ns_str = ns_from_py(':') + self.assertEqual(ns_str.join(['aa', 'bb', 'cc']), 'aa:bb:cc') + + def test_nsstring_ljust(self): + ns_str = ns_from_py('123') + self.assertEqual(ns_str.ljust(5), '123 ') + self.assertEqual(ns_str.ljust(5, '*'), '123**') + + def test_nsstring_lower(self): + ns_str = ns_from_py('lower, UPPER & Mixed!') + self.assertEqual(ns_str.lower(), 'lower, upper & mixed!') + + def test_nsstring_lstrip(self): + ns_str = ns_from_py(' hello ') + self.assertEqual(ns_str.lstrip(), 'hello ') + + ns_str = ns_from_py('...hello...') + self.assertEqual(ns_str.lstrip('.'), 'hello...') + + def test_nsstring_maketrans(self): + ns_str = ns_from_py('hello') + self.assertEqual(ns_str.maketrans('lo', 'g!'), {108: 103, 111: 33}) + + def test_nsstring_partition(self): + ns_str = ns_from_py('hello new world') + self.assertEqual(ns_str.partition(' '), ('hello', ' ', 'new world')) + self.assertEqual(ns_str.partition('l'), ('he', 'l', 'lo new world')) + + def test_nsstring_replace(self): + ns_str = ns_from_py('hello new world') + self.assertEqual(ns_str.replace('new', 'old'), 'hello old world') + self.assertEqual(ns_str.replace('l', '!'), 'he!!o new wor!d') + self.assertEqual(ns_str.replace('l', '!', 2), 'he!!o new world') + + def test_nsstring_rjust(self): + ns_str = ns_from_py('123') + self.assertEqual(ns_str.rjust(5), ' 123') + self.assertEqual(ns_str.rjust(5, '*'), '**123') + + def test_nsstring_rpartition(self): + ns_str = ns_from_py('hello new world') + self.assertEqual(ns_str.rpartition(' '), ('hello new', ' ', 'world')) + self.assertEqual(ns_str.rpartition('l'), ('hello new wor', 'l', 'd')) + + def test_nsstring_rsplit(self): + ns_str = ns_from_py('hello new world') + self.assertEqual(ns_str.rsplit(), ['hello', 'new', 'world']) + self.assertEqual(ns_str.rsplit(' ', 1), ['hello new', 'world']) + self.assertEqual(ns_str.rsplit('l'), ['he', '', 'o new wor', 'd']) + self.assertEqual(ns_str.rsplit('l', 2), ['hel', 'o new wor', 'd']) + + def test_nsstring_rstrip(self): + ns_str = ns_from_py(' hello ') + self.assertEqual(ns_str.rstrip(), ' hello') + + ns_str = ns_from_py('...hello...') + self.assertEqual(ns_str.rstrip('.'), '...hello') + + def test_nsstring_split(self): + ns_str = ns_from_py('hello new world') + self.assertEqual(ns_str.split(), ['hello', 'new', 'world']) + self.assertEqual(ns_str.split(' ', 1), ['hello', 'new world']) + self.assertEqual(ns_str.split('l'), ['he', '', 'o new wor', 'd']) + self.assertEqual(ns_str.split('l', 2), ['he', '', 'o new world']) + + def test_nsstring_splitlines(self): + ns_str = ns_from_py('Hello\nnew\nworld\n') + self.assertEqual(ns_str.splitlines(), ['Hello', 'new', 'world']) + + def test_nsstring_strip(self): + ns_str = ns_from_py(' hello ') + self.assertEqual(ns_str.strip(), 'hello') + + ns_str = ns_from_py('...hello...') + self.assertEqual(ns_str.strip('.'), 'hello') + + def test_nsstring_swapcase(self): + ns_str = ns_from_py('lower, UPPER & Mixed!') + self.assertEqual(ns_str.swapcase(), 'LOWER, upper & mIXED!') + + def test_nsstring_title(self): + ns_str = ns_from_py('lower, UPPER & Mixed!') + self.assertEqual(ns_str.title(), 'Lower, Upper & Mixed!') + + def test_nsstring_translate(self): + ns_str = ns_from_py('hello') + self.assertEqual(ns_str.translate({108: 'g', 111: '!!'}), 'hegg!!') + + def test_nsstring_upper(self): + ns_str = ns_from_py('lower, UPPER & Mixed!') + self.assertEqual(ns_str.upper(), 'LOWER, UPPER & MIXED!') + + def test_nsstring_zfill(self): + ns_str = ns_from_py('123') + self.assertEqual(ns_str.zfill(5), '00123') From 35a41cc8a38cb64e9e306db1aab9c5ef1e12b24c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 27 May 2018 16:39:31 +0200 Subject: [PATCH 07/30] Clarified the docs around NSString method mapping. --- docs/how-to/type-mapping.rst | 129 ++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/docs/how-to/type-mapping.rst b/docs/how-to/type-mapping.rst index bfaa9dc1..b1606716 100644 --- a/docs/how-to/type-mapping.rst +++ b/docs/how-to/type-mapping.rst @@ -29,94 +29,99 @@ Primitives If a Python value needs to be passed in as a primitive, Rubicon will wrap the primitive: -===== ============================================================= -Value C primitive -===== ============================================================ -bool 8 bit integer (although it can only hold 2 values - 0 and 1) -int 32 bit integer -float double precision floating point -===== ============================================================ +============== ============================================================ +Value C primitive +============== ============================================================ +:class:`bool` 8 bit integer (although it can only hold 2 values - 0 and 1) +:class:`int` 32 bit integer +:class:`float` double precision floating point +============== ============================================================ If a Python value needs to be passed in as an object, Rubicon will wrap the primitive in an object: -===== ================= -Value Objective C type -===== ================= -bool NSNumber (bool) -int NSNumber (long) -float NSNumber (double) -===== ================= +============== ========================== +Value Objective C type +============== ========================== +:class:`bool` :class:`NSNumber` (bool) +:class:`int` :class:`NSNumber` (long) +:class:`float` :class:`NSNumber` (double) +============== ========================== If you're declaring a method and need to annotate the type of an argument, the Python type name can be used as the annotation type. You can also use any of -the `ctypes` primitive types. Rubicon also provides type definitions for common -Objective-C typedefs, like `NSInteger`, `CGFloat`, and so on. +the ``ctypes`` primitive types. Rubicon also provides type definitions for common +Objective-C typedefs, like :class:`NSInteger`, :class:`CGFloat`, and so on. Strings ------- -If a method calls for an `NSString` argument, you can provide a Python `str` -for that argument. Rubicon will construct an `NSString` instance from the data -in the `str` provided, and pass that value for the argument. - -If a method returns an `NSString`, the return value will be a wrapped -`ObjCStrInstance` type. This type implements a `str`-like interface, wrapped -around the underlying `NSString` data. This means you can treat the return -value as if it were a string - slicing it, concatenating it with other strings, -comparing it, and so on. - -Note that `ObjCStrInstance` objects behave slightly differently than Python -`str` objects in some cases. For technical reasons, `ObjCStrInstance` objects -are not hashable, which means they cannot be used as `dict` keys (but they -*can* be used as `NSDictionary` keys). `ObjCStrInstance` also handles Unicode -code points above U+FFFF differently than Python `str`, because the underlying -`NSString` is based on UTF-16. - -At the moment `ObjCStrInstance` does not yet support many methods that are -available on `str`. More methods will be implemented in the future, such as -`replace` and `split`. However some methods will likely never be available on -`ObjCStrInstance` as they would be too complex to reimplement, such as `format` -and `encode`. If you need to use a method that `ObjCStrInstance` doesn't -support, you can use `str(nsstring)` to convert it to `str`. +If a method calls for an :class:`NSString` argument, you can provide a Python +:class:`str` for that argument. Rubicon will construct an :class:`NSString` +instance from the data in the :class:`str` provided, and pass that value for +the argument. + +If a method returns an :class:`NSString`, the return value will be a wrapped +:class:`ObjCStrInstance` type. This type implements a :class:`str`-like +interface, wrapped around the underlying :class:`NSString` data. This means +you can treat the return value as if it were a string - slicing it, +concatenating it with other strings, comparing it, and so on. + +Note that :class:`ObjCStrInstance` objects behave slightly differently than +Python :class:`str` objects in some cases. For technical reasons, +:class:`ObjCStrInstance` objects are not hashable, which means they cannot be +used as :class:`dict` keys (but they *can* be used as :class:`NSDictionary` +keys). :class:`ObjCStrInstance` also handles Unicode code points above +``U+FFFF`` differently than Python :class:`str`, because the underlying +:class:`NSString` is based on UTF-16. + +If you have an :class:`ObjCStrInstance` instance, and you need to pass that +instance to a method that does a specific typecheck for `str`, you can use +:class:`str(nsstring)` to convert the :class:`ObjCStrInstance` instance to +:class:`str`. + +:class:`ObjCStrInstance` implements all the utility methods that are available +on :class:`str`, such as ``replace`` and ``split``. These utility methods all +return *Python* strings. Lists ----- -If a method calls for an `NSArray` or `NSMutableArray` argument, you can -provide a Python `list` for that argument. Rubicon will construct an -`NSMutableArray` instance from the data in the `list` provided, and pass that -value for the argument. +If a method calls for an :class:`NSArray` or :class:`NSMutableArray` argument, +you can provide a Python :class:`list` for that argument. Rubicon will +construct an :class:`NSMutableArray` instance from the data in the +:class:`list` provided, and pass that value for the argument. -If a method returns an `NSArray` or `NSMutableArray`, the return value will be -a wrapped `ObjCListInstance` type. This type implements a `list`-like -interface, wrapped around the underlying `NSArray` data. This means you can -treat the return value as if it were a list - iterating over values, retrieving -objects by index, and so on. +If a method returns an :class:`NSArray` or :class:`NSMutableArray`, the return +value will be a wrapped :class:`ObjCListInstance` type. This type implements a +:class:`list`-like interface, wrapped around the underlying :class:`NSArray` +data. This means you can treat the return value as if it were a list - +iterating over values, retrieving objects by index, and so on. Dictionaries ------------ -If a method calls for an `NSDictionary` or `NSMutableDictionary` argument, you -can provide a Python `dict`. Rubicon will construct an `NSMutableDictionary` -instance from the data in the `dict` provided, and pass that value for the -argument. +If a method calls for an :class:`NSDictionary` or :class:`NSMutableDictionary` +argument, you can provide a Python :class:`dict`. Rubicon will construct an +:class:`NSMutableDictionary` instance from the data in the :class:`dict` +provided, and pass that value for the argument. -If a method returns an `NSDictionary` or `NSMutableDictionary`, the return -value will be a wrapped `ObjCDictInstance` type. This type implements a -`dict`-like interface, wrapped around the underlying `NSDictionary` data. This -means you can treat the return value as if it were a dict - iterating over -keys, values or items, retrieving objects by key, and so on. +If a method returns an :class:`NSDictionary` or :class:`NSMutableDictionary`, +the return value will be a wrapped :class:`ObjCDictInstance` type. This type +implements a :class:`dict`-like interface, wrapped around the underlying +:class:`NSDictionary` data. This means you can treat the return value as if it +were a dict - iterating over keys, values or items, retrieving objects by key, +and so on. -`NSPoint`, `NSSize`, and `NSRect` ---------------------------------- +:class:`NSPoint`, :class:`NSSize`, and :class:`NSRect` +------------------------------------------------------ On instances of an Objective C structure, each field is exposed as a Python -attribute. For example, if you create an instance of an `NSSize` object you can -access its width and height by calling `NSSize.width`. +attribute. For example, if you create an instance of an :class:`NSSize` object +you can access its width and height by calling :meth:`NSSize.width`. When you need to pass an Objective C structure to an Objective C method, you can pass a tuple instead. For example, if you pass (10.0, 5.1) where a -`NSSize` is expected, it will be converted automatically in the appropriate +:class:`NSSize` is expected, it will be converted automatically in the appropriate width, height for the structure. From 6d64cf709ea18ffc2da07545de9bc09bf0c85abe Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 28 May 2018 10:45:58 +0200 Subject: [PATCH 08/30] Fixed flake8 errors. --- tests/test_NSString.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_NSString.py b/tests/test_NSString.py index c154f8bf..ea684376 100644 --- a/tests/test_NSString.py +++ b/tests/test_NSString.py @@ -220,8 +220,14 @@ def test_nsstring_count(self): def test_nsstring_encode(self): ns_str = ns_from_py('Uñîçö∂€ string') - self.assertEqual(ns_str.encode('utf-8'), b'U\xc3\xb1\xc3\xae\xc3\xa7\xc3\xb6\xe2\x88\x82\xe2\x82\xac string') - self.assertEqual(ns_str.encode('utf-16'), b'\xff\xfeU\x00\xf1\x00\xee\x00\xe7\x00\xf6\x00\x02"\xac \x00s\x00t\x00r\x00i\x00n\x00g\x00') + self.assertEqual( + ns_str.encode('utf-8'), + b'U\xc3\xb1\xc3\xae\xc3\xa7\xc3\xb6\xe2\x88\x82\xe2\x82\xac string' + ) + self.assertEqual( + ns_str.encode('utf-16'), + b'\xff\xfeU\x00\xf1\x00\xee\x00\xe7\x00\xf6\x00\x02"\xac \x00s\x00t\x00r\x00i\x00n\x00g\x00' + ) with self.assertRaises(UnicodeEncodeError): ns_str.encode('ascii') @@ -267,7 +273,6 @@ def test_nsstring_isdigit(self): self.assertFalse(ns_from_py('abcd1234').isdigit()) def test_nsstring_isidentifier(self): - ns_str = ns_from_py('') self.assertTrue(ns_from_py('def').isidentifier()) self.assertTrue(ns_from_py('class').isidentifier()) self.assertTrue(ns_from_py('hello').isidentifier()) From 835603d5032cf6c8ed2320e661c3465abdbbe489 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 28 May 2018 10:55:55 +0200 Subject: [PATCH 09/30] Added fspath handler to ObjCStrInstance. --- rubicon/objc/collections.py | 3 +++ tests/test_NSString.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/rubicon/objc/collections.py b/rubicon/objc/collections.py index fd4c5f03..b9a6d3ec 100644 --- a/rubicon/objc/collections.py +++ b/rubicon/objc/collections.py @@ -32,6 +32,9 @@ class ObjCStrInstance(ObjCInstance): 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)) diff --git a/tests/test_NSString.py b/tests/test_NSString.py index ea684376..09b0817d 100644 --- a/tests/test_NSString.py +++ b/tests/test_NSString.py @@ -1,4 +1,5 @@ import faulthandler +import os import unittest from ctypes import CDLL, util @@ -62,6 +63,15 @@ def test_str_eq_nsstring(self): self.assertEqual(py_second, ns_second) self.assertEqual(ns_second, py_second) + def test_nsstring_as_fspath(self): + """An NSString can be interpreted as a 'path-like' object""" + + # os.path.dirname requires a 'path-like' object. + self.assertEqual( + os.path.dirname(ns_from_py('/path/base/leaf')), + '/path/base' + ) + def test_nsstring_compare(self): """A NSString can be compared to other strings.""" From 91f73cf1ae68ed44e3f0b28541ed1a1b5041352e Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 29 May 2018 21:29:17 +0200 Subject: [PATCH 10/30] Split rubicon.objc.runtime into two modules (runtime and api) rubicon.objc.runtime contains low-level code for interacting with the Objective-C runtime: ctypes type definitions, C function declarations, and Python functions that operate on raw pointers (objc_id) rather than high-level classes (ObjCInstance). rubicon.objc.api contains high-level classes and functions like ObjCInstance, objc_method, and ns_from_py. The .runtime module must not have any explicit dependencies on the .api module. However, certain functions in .runtime also accept instances of classes from .api. This is done by using the objects' _as_parameter_ attribute, which contains the low-level representation of a high-level object. --- docs/how-to/protocols.rst | 6 +- docs/tutorial/tutorial-1.rst | 4 +- rubicon/objc/__init__.py | 10 +- rubicon/objc/api.py | 1471 +++++++++++++++++++++++++++++++++ rubicon/objc/collections.py | 5 +- rubicon/objc/eventloop.py | 3 +- rubicon/objc/runtime.py | 1496 +--------------------------------- tests/test_NSString.py | 2 +- tests/test_blocks.py | 3 +- tests/test_core.py | 3 +- 10 files changed, 1505 insertions(+), 1498 deletions(-) create mode 100644 rubicon/objc/api.py diff --git a/docs/how-to/protocols.rst b/docs/how-to/protocols.rst index 882e93ac..938d85fc 100644 --- a/docs/how-to/protocols.rst +++ b/docs/how-to/protocols.rst @@ -16,7 +16,7 @@ similar to how classes can be looked up using ``ObjCClass``: >>> NSCopying = ObjCProtocol('NSCopying') >>> NSCopying - + The ``isinstance`` function can be used to check whether an object conforms to a protocol: @@ -67,9 +67,9 @@ We can now use our class. The ``copy`` method (which uses our implemented >>> ua = UserAccount.alloc().initWithUsername_emailAddress_(at('person'), at('person@example.com')) >>> ua - > + > >>> ua.copy() - > + > And we can check that the class conforms to the protocol: diff --git a/docs/tutorial/tutorial-1.rst b/docs/tutorial/tutorial-1.rst index a617a514..9e5f3638 100644 --- a/docs/tutorial/tutorial-1.rst +++ b/docs/tutorial/tutorial-1.rst @@ -93,10 +93,10 @@ equivalents, `__str__()` and `__repr__()`, respectively:: 'http://pybee.org/contributing/how/first-time/' >>> repr(absolute) - '' + '' >>> print(absolute) - + Time to take over the world! ---------------------------- diff --git a/rubicon/objc/__init__.py b/rubicon/objc/__init__.py index 060f7d9c..8692e19c 100644 --- a/rubicon/objc/__init__.py +++ b/rubicon/objc/__init__.py @@ -12,10 +12,12 @@ from . import collections # noqa: F401 from .runtime import ( # noqa: F401 - IMP, SEL, Block, Class, Ivar, Method, NSArray, NSDictionary, NSMutableArray, NSMutableDictionary, NSObject, - NSObjectProtocol, ObjCBlock, ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, get_ivar, ns_from_py, - objc_classmethod, objc_const, objc_id, objc_ivar, objc_method, objc_property, objc_property_t, objc_rawmethod, - py_from_ns, send_message, send_super, set_ivar, + IMP, SEL, Class, Ivar, Method, get_ivar, objc_id, objc_property_t, send_message, send_super, set_ivar, +) +from .api import ( # noqa: F401 + Block, NSArray, NSDictionary, NSMutableArray, NSMutableDictionary, NSObject, NSObjectProtocol, ObjCBlock, + ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, ns_from_py, objc_classmethod, objc_const, objc_ivar, + objc_method, objc_property, objc_rawmethod, py_from_ns, ) from .types import ( # noqa: F401 CFIndex, CFRange, CGFloat, CGGlyph, CGPoint, CGPointMake, CGRect, diff --git a/rubicon/objc/api.py b/rubicon/objc/api.py new file mode 100644 index 00000000..44c4ac40 --- /dev/null +++ b/rubicon/objc/api.py @@ -0,0 +1,1471 @@ +import collections.abc +import decimal +import enum +import inspect +from ctypes import ( + CFUNCTYPE, POINTER, ArgumentError, Array, Structure, Union, addressof, byref, c_bool, c_char_p, c_int, c_uint, + c_uint8, c_ulong, c_void_p, cast, sizeof, string_at +) + +from .types import ( + compound_value_for_sequence, ctype_for_type, ctypes_for_method_encoding, encoding_for_ctype, + register_ctype_for_type +) +from .runtime import ( + Class, SEL, add_ivar, add_method, ensure_bytes, get_class, get_ivar, libc, libobjc, objc_block, objc_id, + objc_property_attribute_t, object_isClass, set_ivar, send_message, send_super, +) + +__all__ = [ + 'Block', + 'BlockConsts', + 'BlockDescriptor', + 'BlockLiteral', + 'DeallocationObserver', + 'NSObject', + 'ObjCBlock', + 'ObjCBlockInstance', + 'ObjCBlockStruct', + 'ObjCBoundMethod', + 'ObjCClass', + 'ObjCInstance', + 'ObjCMetaClass', + 'ObjCMethod', + 'ObjCPartialMethod', + 'ObjCProtocol', + 'at', + 'cache_method', + 'cache_property_accessor', + 'cache_property_methods', + 'cache_property_mutator', + 'cast_block_descriptor', + 'convert_method_arguments', + 'create_block_descriptor_struct', + 'encoding_from_annotation', + 'for_objcclass', + 'get_type_for_objcclass_map', + 'ns_from_py', + 'objc_classmethod', + 'objc_const', + 'objc_ivar', + 'objc_method', + 'objc_property', + 'objc_rawmethod', + 'py_from_ns', + 'register_type_for_objcclass', + 'type_for_objcclass', + 'unregister_type_for_objcclass', +] + + +def encoding_from_annotation(f, offset=1): + argspec = inspect.getfullargspec(inspect.unwrap(f)) + + encoding = [argspec.annotations.get('return', ObjCInstance), ObjCInstance, SEL] + + for varname in argspec.args[offset:]: + encoding.append(argspec.annotations.get(varname, ObjCInstance)) + + return encoding + + +class ObjCMethod(object): + """This represents an unbound Objective-C method (really an IMP).""" + + def __init__(self, method): + """Initialize with an Objective-C Method pointer. We then determine + the return type and argument type information of the method.""" + self.selector = libobjc.method_getName(method) + self.name = self.selector.name + self.pyname = self.name.replace(b':', b'_') + self.encoding = libobjc.method_getTypeEncoding(method) + self.restype, *self.argtypes = ctypes_for_method_encoding(self.encoding) + self.imp = libobjc.method_getImplementation(method) + self.func = None + + def get_prototype(self): + """Returns a ctypes CFUNCTYPE for the method.""" + return CFUNCTYPE(self.restype, *self.argtypes) + + def __repr__(self): + return "" % (self.name, self.encoding) + + def get_callable(self): + """Returns a python-callable version of the method's IMP.""" + if not self.func: + self.func = cast(self.imp, self.get_prototype()) + self.func.restype = self.restype + self.func.argtypes = self.argtypes + return self.func + + def __call__(self, receiver, *args, convert_args=True, convert_result=True): + """Call the method with the given id and arguments. You do not need + to pass in the selector as an argument since it will be automatically + provided.""" + f = self.get_callable() + + if convert_args: + converted_args = [] + for argtype, arg in zip(self.argtypes[2:], args): + if isinstance(arg, enum.Enum): + # Convert Python enum objects to their values + arg = arg.value + + if issubclass(argtype, objc_block): + if arg is None: + # allow for 'nil' block args, which some objc methods accept + arg = ns_from_py(arg) + elif (callable(arg) and + not isinstance(arg, Block)): # <-- guard against someone someday making Block callable + # Note: We need to keep the temp. Block instance + # around at least until the objc method is called. + # _as_parameter_ is used in the actual ctypes marshalling below. + arg = Block(arg) + # ^ For blocks at this point either arg is a Block instance + # (making use of _as_parameter_), is None, or if it isn't either of + # those two, an ArgumentError will be raised below. + elif issubclass(argtype, objc_id): + # Convert Python objects to Foundation objects + arg = ns_from_py(arg) + elif isinstance(arg, collections.abc.Iterable) and issubclass(argtype, (Structure, Array)): + arg = compound_value_for_sequence(arg, argtype) + + converted_args.append(arg) + else: + converted_args = args + + try: + result = f(receiver, self.selector, *converted_args) + except ArgumentError as error: + # Add more useful info to argument error exceptions, then reraise. + error.args = ( + error.args[0] + + ' (selector = {self.name}, argtypes = {self.argtypes}, encoding = {self.encoding})' + .format(self=self), + ) + raise + else: + if not convert_result: + return result + + # Convert result to python type if it is a instance or class pointer. + if self.restype is not None and issubclass(self.restype, objc_id): + result = py_from_ns(result, _auto=True) + return result + + +class ObjCPartialMethod(object): + _sentinel = object() + + def __init__(self, name_start): + super().__init__() + + self.name_start = name_start + self.methods = {} + + def __repr__(self): + return "{cls.__module__}.{cls.__qualname__}({self.name_start!r})".format(cls=type(self), self=self) + + def __call__(self, receiver, first_arg=_sentinel, **kwargs): + if first_arg is ObjCPartialMethod._sentinel: + if kwargs: + raise TypeError("Missing first (positional) argument") + + args = [] + rest = frozenset() + else: + args = [first_arg] + # Add "" to rest to indicate that the method takes arguments + rest = frozenset(kwargs) | frozenset(("",)) + + try: + meth, order = self.methods[rest] + except KeyError: + raise ValueError( + "No method was found starting with {!r} and with selector parts {}\nKnown selector parts are:\n{}" + .format(self.name_start, set(kwargs), "\n".join(repr(parts) for parts in self.methods)) + ) + + meth = ObjCMethod(meth) + args += [kwargs[name] for name in order] + return meth(receiver, *args) + + +class ObjCBoundMethod(object): + """This represents an Objective-C method (an IMP) which has been bound + to some id which will be passed as the first parameter to the method.""" + + def __init__(self, method, receiver): + """Initialize with a method and ObjCInstance or ObjCClass object.""" + self.method = method + if type(receiver) == Class: + self.receiver = cast(receiver, objc_id) + else: + self.receiver = receiver + + def __repr__(self): + return '{cls.__module__}.{cls.__qualname__}({self.method}, {self.receiver})'.format( + cls=type(self), self=self) + + def __call__(self, *args, **kwargs): + """Call the method with the given arguments.""" + return self.method(self.receiver, *args, **kwargs) + + +def cache_method(cls, name): + """Returns a python representation of the named instance method, + either by looking it up in the cached list of methods or by searching + for and creating a new method object.""" + + supercls = cls + objc_method = None + while supercls is not None: + # Load the class's methods if we haven't done so yet. + if supercls.methods_ptr is None: + supercls._load_methods() + + try: + objc_method = supercls.instance_methods[name] + break + except KeyError: + pass + + try: + objc_method = ObjCMethod(supercls.instance_method_ptrs[name]) + break + except KeyError: + pass + + supercls = supercls.superclass + + if objc_method is None: + return None + else: + cls.instance_methods[name] = objc_method + return objc_method + + +def cache_property_methods(cls, name): + """Return the accessor and mutator for the named property. + """ + if name.endswith('_'): + # If the requested name ends with _, that's a marker that we're + # dealing with a method call, not a property, so we can shortcut + # the process. + methods = None + else: + # Check 1: Does the class respond to the property? + responds = libobjc.class_getProperty(cls, name.encode('utf-8')) + + # Check 2: Does the class have an instance method to retrieve the given name + accessor = cache_method(cls, name) + + # Check 3: Is there a setName: method to set the property with the given name + mutator = cache_method(cls, 'set' + name[0].title() + name[1:] + ':') + + # Check 4: Is this a forced property on this class or a superclass? + forced = False + superclass = cls + while superclass is not None: + if name in superclass.forced_properties: + forced = True + break + superclass = superclass.superclass + + # If the class responds as a property, or it has both an accessor *and* + # and mutator, then treat it as a property in Python. + if responds or (accessor and mutator) or forced: + methods = (accessor, mutator) + else: + methods = None + return methods + + +def cache_property_accessor(cls, name): + """Returns a python representation of an accessor for the named + property. Existence of a property is done by looking for the write + selector (set:). + """ + try: + methods = cls.instance_properties[name] + except KeyError: + methods = cache_property_methods(cls, name) + cls.instance_properties[name] = methods + if methods: + return methods[0] + return None + + +def cache_property_mutator(cls, name): + """Returns a python representation of an accessor for the named + property. Existence of a property is done by looking for the write + selector (set:). + """ + try: + methods = cls.instance_properties[name] + except KeyError: + methods = cache_property_methods(cls, name) + cls.instance_properties[name] = methods + if methods: + return methods[1] + return None + + +def convert_method_arguments(encoding, args): + """Used to convert Objective-C method arguments to Python values + before passing them on to the Python-defined method. + """ + new_args = [] + for e, a in zip(encoding[3:], args): + if issubclass(e, (objc_id, ObjCInstance)): + new_args.append(py_from_ns(a, _auto=True)) + else: + new_args.append(a) + return new_args + + +def objc_method(f): + encoding = encoding_from_annotation(f) + + def _objc_method(receiver, objc_cmd, *args): + py_self = ObjCInstance(receiver) + args = convert_method_arguments(encoding, args) + result = f(py_self, *args) + if encoding[0] is not None and issubclass(encoding[0], (objc_id, ObjCInstance)): + result = ns_from_py(result) + if result is not None: + result = result.ptr + if isinstance(result, c_void_p): + return result.value + else: + return result + + def register(cls, attr): + name = attr.replace("_", ":") + cls.imp_keep_alive_table[name] = add_method(cls, name, _objc_method, encoding) + + def protocol_register(proto, attr): + name = attr.replace('_', ':') + types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding) + libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, True) + + _objc_method.register = register + _objc_method.protocol_register = protocol_register + + return _objc_method + + +def objc_classmethod(f): + encoding = encoding_from_annotation(f) + + def _objc_classmethod(objc_cls, objc_cmd, *args): + py_cls = ObjCClass(objc_cls) + args = convert_method_arguments(encoding, args) + result = f(py_cls, *args) + if encoding[0] is not None and issubclass(encoding[0], (objc_id, ObjCInstance)): + result = ns_from_py(result) + if result is not None: + result = result.ptr + if isinstance(result, c_void_p): + return result.value + else: + return result + + def register(cls, attr): + name = attr.replace("_", ":") + cls.imp_keep_alive_table[name] = add_method(cls.objc_class, name, _objc_classmethod, encoding) + + def protocol_register(proto, attr): + name = attr.replace('_', ':') + types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding) + libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, False) + + _objc_classmethod.register = register + _objc_classmethod.protocol_register = protocol_register + + return _objc_classmethod + + +class objc_ivar(object): + """Add an instance variable of type vartype to the subclass. + vartype is a ctypes type. + The class must be registered AFTER adding instance variables. + """ + def __init__(self, vartype): + self.vartype = vartype + + def pre_register(self, ptr, attr): + return add_ivar(ptr, attr, self.vartype) + + def protocol_register(self, proto, attr): + raise TypeError('Objective-C protocols cannot have ivars') + + +class objc_property(object): + """Add a property to an Objective-C class. + + An ivar, a getter and a setter are automatically generated. + If the property's type is objc_id or a subclass, the generated setter keeps the stored object retained, and + releases it when it is replaced. + """ + + def __init__(self, vartype=objc_id): + super().__init__() + + self.vartype = ctype_for_type(vartype) + + def _get_property_attributes(self): + attrs = [ + objc_property_attribute_t(b'T', encoding_for_ctype(self.vartype)), # Type: vartype + ] + if issubclass(self.vartype, objc_id): + attrs.append(objc_property_attribute_t(b'&', b'')) # retain + return (objc_property_attribute_t * len(attrs))(*attrs) + + def pre_register(self, ptr, attr): + add_ivar(ptr, '_' + attr, self.vartype) + + def register(self, cls, attr): + def _objc_getter(objc_self, _cmd): + value = get_ivar(objc_self, '_' + attr) + # ctypes complains when a callback returns a "boxed" primitive type, so we have to manually unbox it. + # If the data object has a value attribute and is not a structure or union, assume that it is + # a primitive and unbox it. + if not isinstance(value, (Structure, Union)): + try: + value = value.value + except AttributeError: + pass + + return value + + def _objc_setter(objc_self, _cmd, new_value): + if not isinstance(new_value, self.vartype): + # If vartype is a primitive, then new_value may be unboxed. If that is the case, box it manually. + new_value = self.vartype(new_value) + old_value = get_ivar(objc_self, '_' + attr) + if issubclass(self.vartype, objc_id) and new_value: + # If the new value is a non-null object, retain it. + send_message(new_value, 'retain', restype=objc_id, argtypes=[]) + set_ivar(objc_self, '_' + attr, new_value) + if issubclass(self.vartype, objc_id) and old_value: + # If the old value is a non-null object, release it. + send_message(old_value, 'release', restype=None, argtypes=[]) + + setter_name = 'set' + attr[0].upper() + attr[1:] + ':' + + cls.imp_keep_alive_table[attr] = add_method( + cls.ptr, attr, _objc_getter, + [self.vartype, ObjCInstance, SEL], + ) + cls.imp_keep_alive_table[setter_name] = add_method( + cls.ptr, setter_name, _objc_setter, + [None, ObjCInstance, SEL, self.vartype], + ) + + attrs = self._get_property_attributes() + libobjc.class_addProperty(cls, ensure_bytes(attr), attrs, len(attrs)) + + def protocol_register(self, proto, attr): + attrs = self._get_property_attributes() + libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs, len(attrs), True, True) + + +def objc_rawmethod(f): + encoding = encoding_from_annotation(f, offset=2) + + def register(cls, attr): + name = attr.replace("_", ":") + cls.imp_keep_alive_table[name] = add_method(cls, name, f, encoding) + + def protocol_register(proto, attr): + raise TypeError('Protocols cannot have method implementations, use objc_method instead of objc_rawmethod') + + f.register = register + f.protocol_register = protocol_register + + return f + + +_type_for_objcclass_map = {} + + +def type_for_objcclass(objcclass): + """Look up the ObjCInstance subclass used to represent instances of the given Objective-C class in Python. + + If the exact Objective-C class is not registered, each superclass is also checked, defaulting to ObjCInstance + if none of the classes in the superclass chain is registered. Afterwards, all searched superclasses are registered + for the ObjCInstance subclass that was found. + """ + + if isinstance(objcclass, ObjCClass): + objcclass = objcclass.ptr + + superclass = objcclass + traversed_classes = [] + pytype = ObjCInstance + while superclass.value is not None: + try: + pytype = _type_for_objcclass_map[superclass.value] + except KeyError: + traversed_classes.append(superclass) + superclass = libobjc.class_getSuperclass(superclass) + else: + break + + for cls in traversed_classes: + register_type_for_objcclass(pytype, cls) + + return pytype + + +def register_type_for_objcclass(pytype, objcclass): + """Register a conversion from an Objective-C class to an ObjCInstance subclass.""" + + if isinstance(objcclass, ObjCClass): + objcclass = objcclass.ptr + + _type_for_objcclass_map[objcclass.value] = pytype + + +def unregister_type_for_objcclass(objcclass): + """Unregister a conversion from an Objective-C class to an ObjCInstance subclass""" + + if isinstance(objcclass, ObjCClass): + objcclass = objcclass.ptr + + del _type_for_objcclass_map[objcclass.value] + + +def get_type_for_objcclass_map(): + """Get a copy of all currently registered ObjCInstance subclasses as a mapping. + Keys are Objective-C class addresses as integers. + """ + + return dict(_type_for_objcclass_map) + + +def for_objcclass(objcclass): + """Decorator for registering a conversion from an Objective-C class to an ObjCInstance subclass. + This is equivalent to calling register_type_for_objcclass. + """ + + def _for_objcclass(pytype): + register_type_for_objcclass(pytype, objcclass) + return pytype + + return _for_objcclass + + +class ObjCInstance(object): + """Python wrapper for an Objective-C instance.""" + + _cached_objects = {} + + @property + def objc_class(self): + return ObjCClass(libobjc.object_getClass(self)) + + def __new__(cls, object_ptr, _name=None, _bases=None, _ns=None): + """Create a new ObjCInstance or return a previously created one + for the given object_ptr which should be an Objective-C id.""" + # Make sure that object_ptr is wrapped in an objc_id. + if not isinstance(object_ptr, objc_id): + object_ptr = cast(object_ptr, objc_id) + + # If given a nil pointer, return None. + if not object_ptr.value: + return None + + # Check if we've already created a Python ObjCInstance for this + # object_ptr id and if so, then return it. A single ObjCInstance will + # be created for any object pointer when it is first encountered. + # This same ObjCInstance will then persist until the object is + # deallocated. + if object_ptr.value in cls._cached_objects: + return cls._cached_objects[object_ptr.value] + + # If the given pointer points to a class, return an ObjCClass instead (if we're not already creating one). + if not issubclass(cls, ObjCClass) and object_isClass(object_ptr): + return ObjCClass(object_ptr) + + # Otherwise, create a new ObjCInstance. + if issubclass(cls, type): + # Special case for ObjCClass to pass on the class name, bases and namespace to the type constructor. + self = super().__new__(cls, _name, _bases, _ns) + else: + if isinstance(object_ptr, objc_block): + cls = ObjCBlockInstance + else: + cls = type_for_objcclass(libobjc.object_getClass(object_ptr)) + self = super().__new__(cls) + super(ObjCInstance, type(self)).__setattr__(self, "ptr", object_ptr) + super(ObjCInstance, type(self)).__setattr__(self, "_as_parameter_", object_ptr) + if isinstance(object_ptr, objc_block): + super(ObjCInstance, type(self)).__setattr__(self, "block", ObjCBlock(object_ptr)) + # Store new object in the dictionary of cached objects, keyed + # by the (integer) memory address pointed to by the object_ptr. + cls._cached_objects[object_ptr.value] = self + + # Classes are never deallocated, so they don't need a DeallocationObserver. + # This is also necessary to make the definition of DeallocationObserver work - + # otherwise creating the ObjCClass for DeallocationObserver would try to + # instantiate a DeallocationObserver itself. + if not object_isClass(object_ptr): + # Create a DeallocationObserver and associate it with this object. + # When the Objective-C object is deallocated, the observer will remove + # the ObjCInstance corresponding to the object from the cached objects + # dictionary, effectively destroying the ObjCInstance. + observer = send_message( + send_message('DeallocationObserver', 'alloc', restype=objc_id, argtypes=[]), + 'initWithObject:', self, restype=objc_id, argtypes=[objc_id] + ) + libobjc.objc_setAssociatedObject(self, observer, observer, 0x301) + + # The observer is retained by the object we associate it to. We release + # the observer now so that it will be deallocated when the associated + # object is deallocated. + send_message(observer, 'release') + + return self + + def __str__(self): + desc = self.description + if desc is None: + raise ValueError('{self.name}.description returned nil'.format(self=self)) + return str(desc) + + def __repr__(self): + return "<%s.%s %#x: %s at %#x: %s>" % ( + type(self).__module__, + type(self).__qualname__, + id(self), + self.objc_class.name, + self.ptr.value, + self.debugDescription, + ) + + def __getattr__(self, name): + """Returns a callable method object with the given name.""" + # Search for named instance method in the class object and if it + # exists, return callable object with self as hidden argument. + # Note: you should give self and not self.ptr as a parameter to + # ObjCBoundMethod, so that it will be able to keep the ObjCInstance + # alive for chained calls like MyClass.alloc().init() where the + # object created by alloc() is not assigned to a variable. + + # If there's a property with this name; return the value directly. + # If the name ends with _, we can shortcut this step, because it's + # clear that we're dealing with a method call. + if not name.endswith('_'): + method = cache_property_accessor(self.objc_class, name) + if method: + return ObjCBoundMethod(method, self)() + + # See if there's a partial method starting with the given name, + # either on self's class or any of the superclasses. + cls = self.objc_class + while cls is not None: + # Load the class's methods if we haven't done so yet. + if cls.methods_ptr is None: + cls._load_methods() + + try: + method = cls.partial_methods[name] + break + except KeyError: + cls = cls.superclass + else: + method = None + + if method is not None: + # If the partial method can only resolve to one method that takes no arguments, + # return that method directly, instead of a mostly useless partial method. + if set(method.methods) == {frozenset()}: + method, _ = method.methods[frozenset()] + method = ObjCMethod(method) + + return ObjCBoundMethod(method, self) + + # See if there's a method whose full name matches the given name. + method = cache_method(self.objc_class, name.replace("_", ":")) + if method: + return ObjCBoundMethod(method, self) + else: + raise AttributeError('%s.%s %s has no attribute %s' % ( + type(self).__module__, type(self).__qualname__, self.objc_class.name, name) + ) + + def __setattr__(self, name, value): + if name in self.__dict__: + # For attributes already in __dict__, use the default __setattr__. + super(ObjCInstance, type(self)).__setattr__(self, name, value) + else: + method = cache_property_mutator(self.objc_class, name) + if method: + # Convert enums to their underlying values. + if isinstance(value, enum.Enum): + value = value.value + ObjCBoundMethod(method, self)(value) + else: + super(ObjCInstance, type(self)).__setattr__(self, name, value) + + +# The inheritance order is important here. +# type must come after ObjCInstance, so super() refers to ObjCInstance. +# This allows the ObjCInstance constructor to receive the class pointer +# as well as the name, bases, attrs arguments. +# The other way around this would not be possible, because then +# the type constructor would be called before ObjCInstance's, and there +# would be no opportunity to pass extra arguments. +class ObjCClass(ObjCInstance, type): + """Python wrapper for an Objective-C class.""" + + @property + def superclass(self): + """The superclass of this class, or None if this is a root class (such as NSObject).""" + + super_ptr = libobjc.class_getSuperclass(self) + if super_ptr.value is None: + return None + else: + return ObjCClass(super_ptr) + + @property + def protocols(self): + """The protocols adopted by this class.""" + + out_count = c_uint() + protocols_ptr = libobjc.class_copyProtocolList(self, byref(out_count)) + return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value)) + + @classmethod + def _new_from_name(cls, name): + name = ensure_bytes(name) + ptr = get_class(name) + if ptr.value is None: + raise NameError("ObjC Class '%s' couldn't be found." % name) + + return ptr, name + + @classmethod + def _new_from_ptr(cls, ptr): + ptr = cast(ptr, Class) + if ptr.value is None: + raise ValueError("Cannot create ObjCClass from nil pointer") + elif not object_isClass(ptr): + raise ValueError("Pointer {} ({:#x}) does not refer to a class".format(ptr, ptr.value)) + name = libobjc.class_getName(ptr) + + return ptr, name + + @classmethod + def _new_from_class_statement(cls, name, bases, attrs, *, protocols): + name = ensure_bytes(name) + + if get_class(name).value is not None: + raise RuntimeError('An Objective-C class named {!r} already exists'.format(name)) + + try: + (superclass,) = bases + except ValueError: + raise ValueError('An Objective-C class must have exactly one base class, not {}'.format(len(bases))) + + # Check that the superclass is an ObjCClass. + if not isinstance(superclass, ObjCClass): + raise TypeError( + 'The superclass of an Objective-C class must be an ObjCClass, ' + 'not a {cls.__module__}.{cls.__qualname__}' + .format(cls=type(superclass)) + ) + + # Check that all protocols are ObjCProtocols, and that there are no duplicates. + for proto in protocols: + if not isinstance(proto, ObjCProtocol): + raise TypeError( + 'The protocols list of an Objective-C class must contain ObjCProtocol objects, ' + 'not {cls.__module__}.{cls.__qualname__}' + .format(cls=type(proto)) + ) + elif protocols.count(proto) > 1: + raise ValueError('Protocol {} is adopted more than once'.format(proto.name)) + + # Create the ObjC class description + ptr = libobjc.objc_allocateClassPair(superclass, name, 0) + if ptr is None: + raise RuntimeError('Class pair allocation failed') + + # Adopt all the protocols. + for proto in protocols: + if not libobjc.class_addProtocol(ptr, proto): + raise RuntimeError('Failed to adopt protocol {}'.format(proto.name)) + + # Pre-Register all the instance variables + for attr, obj in attrs.items(): + if hasattr(obj, 'pre_register'): + obj.pre_register(ptr, attr) + + # Register the ObjC class + libobjc.objc_registerClassPair(ptr) + + return ptr, name, attrs + + def __new__(cls, name_or_ptr, bases=None, attrs=None, *, protocols=()): + """Create a new ObjCClass instance or return a previously created instance for the given Objective-C class. + + If called with a single class pointer argument, an ObjCClass for that class pointer is returned. + If called with a single str or bytes argument, the Objective-C with that name is returned. + + If called with three arguments, they must a name, a superclass list, and a namespace dict. A new Objective-C + class with those properties is created and returned. This form is usually called implicitly when subclassing + another ObjCClass. + In the three-argument form, an optional protocols keyword argument is also accepted. If present, it must be + a sequence of ObjCProtocol objects that the new class should adopt. + """ + + if (bases is None) ^ (attrs is None): + raise TypeError('ObjCClass arguments 2 and 3 must be given together') + + if bases is None and attrs is None: + # A single argument provided. If it's a string, treat it as + # a class name. Anything else treat as a class pointer. + + if protocols: + raise ValueError('protocols kwarg is not allowed for the single-argument form of ObjCClass') + + attrs = {} + + if isinstance(name_or_ptr, (bytes, str)): + ptr, name = cls._new_from_name(name_or_ptr) + else: + ptr, name = cls._new_from_ptr(name_or_ptr) + if not issubclass(cls, ObjCMetaClass) and libobjc.class_isMetaClass(ptr): + return ObjCMetaClass(ptr) + else: + ptr, name, attrs = cls._new_from_class_statement(name_or_ptr, bases, attrs, protocols=protocols) + + objc_class_name = name.decode('utf-8') + + new_attrs = { + '_class_inited': False, + 'name': objc_class_name, + 'methods_ptr_count': c_uint(0), + 'methods_ptr': None, + # Mapping of name -> method pointer + 'instance_method_ptrs': {}, + # Mapping of name -> instance method + 'instance_methods': {}, + # Mapping of name -> (accessor method, mutator method) + 'instance_properties': {}, + # Explicitly declared properties + 'forced_properties': set(), + # Mapping of first selector part -> ObjCPartialMethod instances + 'partial_methods': {}, + # Mapping of name -> CFUNCTYPE callback function + # This only contains the IMPs of methods created in Python, + # which need to be kept from being garbage-collected. + # It does not contain any other methods, do not use it for calling methods. + 'imp_keep_alive_table': {}, + } + + # On Python 3.6 and later, the class namespace may contain a __classcell__ attribute that must be passed on + # to type.__new__. See https://docs.python.org/3/reference/datamodel.html#creating-the-class-object + if '__classcell__' in attrs: + new_attrs['__classcell__'] = attrs['__classcell__'] + + # Create the class object. If there is already a cached instance for ptr, + # it is returned and the additional arguments are ignored. + # Logically this can only happen when creating an ObjCClass from an existing + # name or pointer, not when creating a new class. + # If there is no cached instance for ptr, a new one is created and cached. + self = super().__new__(cls, ptr, objc_class_name, (ObjCInstance,), new_attrs) + + if not self._class_inited: + self._class_inited = True + + # Register all the methods, class methods, etc + registered_something = False + for attr, obj in attrs.items(): + if hasattr(obj, "register"): + registered_something = True + obj.register(self, attr) + + # If anything was registered, reload the methods of this class + # (and the metaclass, because there may be new class methods). + if registered_something: + self._load_methods() + self.objc_class._load_methods() + + return self + + def __init__(self, *args, **kwargs): + # Prevent kwargs from being passed on to type.__init__, which does not accept any kwargs in Python < 3.6. + super().__init__(*args) + + def declare_property(self, name): + self.forced_properties.add(name) + + def declare_class_property(self, name): + self.objc_class.forced_properties.add(name) + + def __repr__(self): + return "<%s.%s: %s at %#x>" % ( + type(self).__module__, + type(self).__qualname__, + self.name, + self.ptr.value, + ) + + def __str__(self): + return "{cls.__name__}({self.name!r})".format(cls=type(self), self=self) + + def __del__(self): + libc.free(self.methods_ptr) + + def __instancecheck__(self, instance): + if isinstance(instance, ObjCInstance): + return bool(instance.isKindOfClass(self)) + else: + return False + + def __subclasscheck__(self, subclass): + if isinstance(subclass, ObjCClass): + return bool(subclass.isSubclassOfClass(self)) + else: + raise TypeError( + 'issubclass(X, {self!r}) arg 1 must be an ObjCClass, not {tp.__module__}.{tp.__qualname__}' + .format(self=self, tp=type(subclass)) + ) + + def _load_methods(self): + if self.methods_ptr is not None: + raise RuntimeError("_load_methods cannot be called more than once") + + self.methods_ptr = libobjc.class_copyMethodList(self, byref(self.methods_ptr_count)) + + if self.superclass is not None and self.superclass.methods_ptr is None: + self.superclass._load_methods() + + for i in range(self.methods_ptr_count.value): + method = self.methods_ptr[i] + name = libobjc.method_getName(method).name.decode("utf-8") + self.instance_method_ptrs[name] = method + + first, *rest = name.split(":") + # Selectors end in a colon iff the method takes arguments. + # Because of this, rest must either be empty (method takes no arguments) + # or the last element must be an empty string (method takes arguments). + assert not rest or rest[-1] == "" + + try: + partial = self.partial_methods[first] + except KeyError: + if self.superclass is None: + super_partial = None + else: + super_partial = self.superclass.partial_methods.get(first) + + partial = self.partial_methods[first] = ObjCPartialMethod(first) + if super_partial is not None: + partial.methods.update(super_partial.methods) + + # order is rest without the dummy "" part + order = rest[:-1] + partial.methods[frozenset(rest)] = (method, order) + + +class ObjCMetaClass(ObjCClass): + """Python wrapper for an Objective-C metaclass.""" + + def __new__(cls, name_or_ptr): + if isinstance(name_or_ptr, (bytes, str)): + name = ensure_bytes(name_or_ptr) + ptr = libobjc.objc_getMetaClass(name) + if ptr.value is None: + raise NameError("Objective-C metaclass {} not found".format(name)) + else: + ptr = cast(name_or_ptr, Class) + if ptr.value is None: + raise ValueError("Cannot create ObjCMetaClass for nil pointer") + elif not object_isClass(ptr) or not libobjc.class_isMetaClass(ptr): + raise ValueError("Pointer {} ({:#x}) does not refer to a metaclass".format(ptr, ptr.value)) + + return super().__new__(cls, ptr) + + +register_ctype_for_type(ObjCInstance, objc_id) +register_ctype_for_type(ObjCClass, Class) + + +NSObject = ObjCClass('NSObject') +NSObject.declare_property('debugDescription') +NSObject.declare_property('description') +NSNumber = ObjCClass('NSNumber') +NSDecimalNumber = ObjCClass('NSDecimalNumber') +NSString = ObjCClass('NSString') +NSString.declare_property('UTF8String') +NSData = ObjCClass('NSData') +NSArray = ObjCClass('NSArray') +NSMutableArray = ObjCClass('NSMutableArray') +NSDictionary = ObjCClass('NSDictionary') +NSMutableDictionary = ObjCClass('NSMutableDictionary') +Protocol = ObjCClass('Protocol') + + +def py_from_ns(nsobj, *, _auto=False): + """Convert a Foundation object into an equivalent Python object if possible. + + Currently supported types: + + * ``objc_id``, ``Class``: Wrapped in an ``ObjCInstance`` and converted as below + * ``NSString``: Converted to ``str`` + * ``NSData``: Converted to ``bytes`` + * ``NSDecimalNumber``: Converted to ``decimal.Decimal`` + * ``NSDictionary``: Converted to ``dict``, with all keys and values converted recursively + * ``NSArray``: Converted to ``list``, with all elements converted recursively + * ``NSNumber``: Converted to a ``bool``, ``int`` or ``float`` based on the type of its contents + + Other objects are returned unmodified as an ``ObjCInstance``. + """ + + if isinstance(nsobj, (objc_id, Class)): + nsobj = ObjCInstance(nsobj) + if not isinstance(nsobj, ObjCInstance): + return nsobj + + if nsobj.isKindOfClass(NSDecimalNumber): + return decimal.Decimal(str(nsobj.descriptionWithLocale(None))) + elif nsobj.isKindOfClass(NSNumber): + # Choose the property to access based on the type encoding. The actual conversion is done by ctypes. + # Signed and unsigned integers are in separate cases to prevent overflow with unsigned long longs. + objc_type = nsobj.objCType + if objc_type == b'B': + return nsobj.boolValue + elif objc_type in b'csilq': + return nsobj.longLongValue + elif objc_type in b'CSILQ': + return nsobj.unsignedLongLongValue + elif objc_type in b'fd': + return nsobj.doubleValue + else: + raise TypeError( + 'NSNumber containing unsupported type {!r} cannot be converted to a Python object' + .format(objc_type) + ) + elif _auto: + # If py_from_ns is called implicitly to convert an Objective-C method's return value, only the conversions + # before this branch are performed. If py_from_ns is called explicitly by hand, the additional conversions + # below this branch are performed as well. + # _auto is a private kwarg that is only passed when py_from_ns is called implicitly. In that case, we return + # early and don't attempt any other conversions. + return nsobj + elif nsobj.isKindOfClass(NSString): + return str(nsobj) + elif nsobj.isKindOfClass(NSData): + # Despite the name, string_at converts the data at the address to a bytes object, not str. + return string_at(send_message(nsobj, 'bytes', restype=POINTER(c_uint8), argtypes=[]), nsobj.length) + elif nsobj.isKindOfClass(NSDictionary): + return {py_from_ns(k): py_from_ns(v) for k, v in nsobj.items()} + elif nsobj.isKindOfClass(NSArray): + return [py_from_ns(o) for o in nsobj] + else: + return nsobj + + +def ns_from_py(pyobj): + """Convert a Python object into an equivalent Foundation object. The returned object is autoreleased. + + This function is also available under the name ``at``, because its functionality is very similar to that of the + Objective-C ``@`` operator and literals. + + Currently supported types: + + * ``None``, ``ObjCInstance``: Returned as-is + * ``enum.Enum``: Replaced by their ``value`` and converted as below + * ``str``: Converted to ``NSString`` + * ``bytes``: Converted to ``NSData`` + * ``decimal.Decimal``: Converted to ``NSDecimalNumber`` + * ``dict``: Converted to ``NSDictionary``, with all keys and values converted recursively + * ``list``: Converted to ``NSArray``, with all elements converted recursively + * ``bool``, ``int``, ``float``: Converted to ``NSNumber`` + + Other types cause a ``TypeError``. + """ + + if isinstance(pyobj, enum.Enum): + pyobj = pyobj.value + + # Many Objective-C method calls here use the convert_result=False kwarg to disable automatic conversion of + # return values, because otherwise most of the Objective-C objects would be converted back to Python objects. + if pyobj is None or isinstance(pyobj, ObjCInstance): + return pyobj + elif isinstance(pyobj, str): + return ObjCInstance(NSString.stringWithUTF8String_(pyobj.encode('utf-8'), convert_result=False)) + elif isinstance(pyobj, bytes): + return ObjCInstance(NSData.dataWithBytes(pyobj, length=len(pyobj))) + elif isinstance(pyobj, decimal.Decimal): + return ObjCInstance(NSDecimalNumber.decimalNumberWithString_(pyobj.to_eng_string(), convert_result=False)) + elif isinstance(pyobj, dict): + dikt = NSMutableDictionary.dictionaryWithCapacity(len(pyobj)) + for k, v in pyobj.items(): + dikt.setObject(v, forKey=k) + return dikt + elif isinstance(pyobj, list): + array = NSMutableArray.arrayWithCapacity(len(pyobj)) + for v in pyobj: + array.addObject(v) + return array + elif isinstance(pyobj, bool): + return ObjCInstance(NSNumber.numberWithBool_(pyobj, convert_result=False)) + elif isinstance(pyobj, int): + return ObjCInstance(NSNumber.numberWithLong_(pyobj, convert_result=False)) + elif isinstance(pyobj, float): + return ObjCInstance(NSNumber.numberWithDouble_(pyobj, convert_result=False)) + else: + raise TypeError( + "Don't know how to convert a {cls.__module__}.{cls.__qualname__} to a Foundation object" + .format(cls=type(pyobj)) + ) + + +at = ns_from_py + + +@for_objcclass(Protocol) +class ObjCProtocol(ObjCInstance): + """Python wrapper for an Objective-C protocol.""" + + @property + def name(self): + """The name of this protocol.""" + + return libobjc.protocol_getName(self).decode('utf-8') + + @property + def protocols(self): + """The superprotocols of this protocol.""" + + out_count = c_uint() + protocols_ptr = libobjc.protocol_copyProtocolList(self, byref(out_count)) + return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value)) + + def __new__(cls, name_or_ptr, bases=None, ns=None): + if (bases is None) ^ (ns is None): + raise TypeError('ObjCProtocol arguments 2 and 3 must be given together') + + if bases is None and ns is None: + if isinstance(name_or_ptr, (bytes, str)): + name = ensure_bytes(name_or_ptr) + ptr = libobjc.objc_getProtocol(name) + if ptr.value is None: + raise NameError('Objective-C protocol {} not found'.format(name)) + else: + ptr = cast(name_or_ptr, objc_id) + if ptr.value is None: + raise ValueError('Cannot create ObjCProtocol for nil pointer') + elif not send_message(ptr, 'isKindOfClass:', Protocol, restype=c_bool, argtypes=[objc_id]): + raise ValueError('Pointer {} ({:#x}) does not refer to a protocol'.format(ptr, ptr.value)) + else: + name = ensure_bytes(name_or_ptr) + + if libobjc.objc_getProtocol(name).value is not None: + raise RuntimeError('An Objective-C protocol named {!r} already exists'.format(name)) + + # Check that all bases are protocols. + for base in bases: + if not isinstance(base, ObjCProtocol): + raise TypeError( + 'An Objective-C protocol can only extend ObjCProtocol objects, ' + 'not {cls.__module__}.{cls.__qualname__}' + .format(cls=type(base)) + ) + + # Allocate the protocol object. + ptr = libobjc.objc_allocateProtocol(name) + if ptr is None: + raise RuntimeError('Protocol allocation failed') + + # Adopt all the protocols. + for proto in bases: + libobjc.protocol_addProtocol(ptr, proto) + + # Register all methods and properties. + for attr, obj in ns.items(): + if hasattr(obj, 'protocol_register'): + obj.protocol_register(ptr, attr) + + # Register the protocol object + libobjc.objc_registerProtocol(ptr) + + return super().__new__(cls, ptr) + + def __repr__(self): + return '<{cls.__module__}.{cls.__qualname__}: {self.name} at {self.ptr.value:#x}>'.format( + cls=type(self), self=self) + + def __instancecheck__(self, instance): + if isinstance(instance, ObjCInstance): + return bool(instance.conformsToProtocol(self)) + else: + return False + + def __subclasscheck__(self, subclass): + if isinstance(subclass, ObjCClass): + return bool(subclass.conformsToProtocol(self)) + elif isinstance(subclass, ObjCProtocol): + return bool(libobjc.protocol_conformsToProtocol(subclass, self)) + else: + raise TypeError( + 'issubclass(X, {self!r}) arg 1 must be an ObjCClass or ObjCProtocol, ' + 'not {tp.__module__}.{tp.__qualname__}' + .format(self=self, tp=type(subclass)) + ) + + +# Need to use a different name to avoid conflict with the NSObject class. +# NSObjectProtocol is also the name that Swift uses when importing the NSObject protocol. +NSObjectProtocol = ObjCProtocol('NSObject') + + +# Instances of DeallocationObserver are associated with every +# Objective-C object that gets wrapped inside an ObjCInstance. +# Their sole purpose is to watch for when the Objective-C object +# is deallocated, and then remove the object from the dictionary +# of cached ObjCInstance objects kept by the ObjCInstance class. +# +# The methods of the class defined below are decorated with +# rawmethod() instead of method() because DeallocationObservers +# are created inside of ObjCInstance's __new__ method and we have +# to be careful to not create another ObjCInstance here (which +# happens when the usual method decorator turns the self argument +# into an ObjCInstance), or else get trapped in an infinite recursion. + +# Try to reuse an existing DeallocationObserver class. +# This allows reloading the module without having to restart +# the interpreter, although any changes to DeallocationObserver +# itself are only applied after a restart of course. +try: + DeallocationObserver = ObjCClass("DeallocationObserver") +except NameError: + class DeallocationObserver(NSObject): + + observed_object = objc_ivar(objc_id) + + @objc_rawmethod + def initWithObject_(self, cmd, anObject): + self = send_message(self, 'init', restype=objc_id, argtypes=[]) + if self is not None: + set_ivar(self, 'observed_object', anObject) + return self.value + + @objc_rawmethod + def dealloc(self, cmd) -> None: + anObject = get_ivar(self, 'observed_object') + ObjCInstance._cached_objects.pop(anObject.value, None) + send_super(__class__, self, 'dealloc', restype=None, argtypes=[]) + + @objc_rawmethod + def finalize(self, cmd) -> None: + # Called instead of dealloc if using garbage collection. + # (which would have to be explicitly started with + # objc_startCollectorThread(), so probably not too much reason + # to have this here, but I guess it can't hurt.) + anObject = get_ivar(self, 'observed_object') + ObjCInstance._cached_objects.pop(anObject.value, None) + send_super(__class__, self, 'finalize', restype=None, argtypes=[]) + + +def objc_const(dll, name): + """Create an ObjCInstance from a global pointer variable in a DLL.""" + + return ObjCInstance(objc_id.in_dll(dll, name)) + + +_cfunc_type_block_invoke = CFUNCTYPE(c_void_p, c_void_p) +_cfunc_type_block_dispose = CFUNCTYPE(c_void_p, c_void_p) +_cfunc_type_block_copy = CFUNCTYPE(c_void_p, c_void_p, c_void_p) + + +class ObjCBlockStruct(Structure): + _fields_ = [ + ('isa', c_void_p), + ('flags', c_int), + ('reserved', c_int), + ('invoke', _cfunc_type_block_invoke), + ('descriptor', c_void_p), + ] + + +class BlockDescriptor(Structure): + _fields_ = [ + ('reserved', c_ulong), + ('size', c_ulong), + ('copy_helper', _cfunc_type_block_copy), + ('dispose_helper', _cfunc_type_block_dispose), + ('signature', c_char_p), + ] + + +class BlockLiteral(Structure): + _fields_ = [ + ('isa', c_void_p), + ('flags', c_int), + ('reserved', c_int), + ('invoke', c_void_p), # NB: this must be c_void_p due to variadic nature + ('descriptor', c_void_p) + ] + + +def create_block_descriptor_struct(has_helpers, has_signature): + descriptor_fields = [ + ('reserved', c_ulong), + ('size', c_ulong), + ] + if has_helpers: + descriptor_fields.extend([ + ('copy_helper', _cfunc_type_block_copy), + ('dispose_helper', _cfunc_type_block_dispose), + ]) + if has_signature: + descriptor_fields.extend([ + ('signature', c_char_p), + ]) + return type( + 'ObjCBlockDescriptor', + (Structure, ), + {'_fields_': descriptor_fields} + ) + + +def cast_block_descriptor(block): + descriptor_struct = create_block_descriptor_struct(block.has_helpers, block.has_signature) + return cast(block.struct.contents.descriptor, POINTER(descriptor_struct)) + + +AUTO = object() + + +class BlockConsts: + HAS_COPY_DISPOSE = 1 << 25 + HAS_CTOR = 1 << 26 + IS_GLOBAL = 1 << 28 + HAS_STRET = 1 << 29 + HAS_SIGNATURE = 1 << 30 + + +class ObjCBlock: + def __init__(self, pointer, return_type=AUTO, *arg_types): + if isinstance(pointer, ObjCInstance): + pointer = pointer.ptr + self.pointer = pointer + self.struct = cast(self.pointer, POINTER(ObjCBlockStruct)) + self.has_helpers = self.struct.contents.flags & BlockConsts.HAS_COPY_DISPOSE + self.has_signature = self.struct.contents.flags & BlockConsts.HAS_SIGNATURE + self.descriptor = cast_block_descriptor(self) + self.signature = self.descriptor.contents.signature if self.has_signature else None + if return_type is AUTO: + if arg_types: + raise ValueError('Cannot use arg_types with return_type AUTO') + if not self.has_signature: + raise ValueError('Cannot use AUTO types for blocks without signatures') + return_type, *arg_types = ctypes_for_method_encoding(self.signature) + self.struct.contents.invoke.restype = ctype_for_type(return_type) + self.struct.contents.invoke.argtypes = (objc_id, ) + tuple(ctype_for_type(arg_type) for arg_type in arg_types) + + def __repr__(self): + representation = '" % (self.name, self.encoding) - - def get_callable(self): - """Returns a python-callable version of the method's IMP.""" - if not self.func: - self.func = cast(self.imp, self.get_prototype()) - self.func.restype = self.restype - self.func.argtypes = self.argtypes - return self.func - - def __call__(self, receiver, *args, convert_args=True, convert_result=True): - """Call the method with the given id and arguments. You do not need - to pass in the selector as an argument since it will be automatically - provided.""" - f = self.get_callable() - - if convert_args: - converted_args = [] - for argtype, arg in zip(self.argtypes[2:], args): - if isinstance(arg, enum.Enum): - # Convert Python enum objects to their values - arg = arg.value - - if issubclass(argtype, objc_block): - if arg is None: - # allow for 'nil' block args, which some objc methods accept - arg = ns_from_py(arg) - elif (callable(arg) and - not isinstance(arg, Block)): # <-- guard against someone someday making Block callable - # Note: We need to keep the temp. Block instance - # around at least until the objc method is called. - # _as_parameter_ is used in the actual ctypes marshalling below. - arg = Block(arg) - # ^ For blocks at this point either arg is a Block instance - # (making use of _as_parameter_), is None, or if it isn't either of - # those two, an ArgumentError will be raised below. - elif issubclass(argtype, objc_id): - # Convert Python objects to Foundation objects - arg = ns_from_py(arg) - elif isinstance(arg, collections.abc.Iterable) and issubclass(argtype, (Structure, Array)): - arg = compound_value_for_sequence(arg, argtype) - - converted_args.append(arg) - else: - converted_args = args - - try: - result = f(receiver, self.selector, *converted_args) - except ArgumentError as error: - # Add more useful info to argument error exceptions, then reraise. - error.args = ( - error.args[0] + - ' (selector = {self.name}, argtypes = {self.argtypes}, encoding = {self.encoding})' - .format(self=self), - ) - raise - else: - if not convert_result: - return result - - # Convert result to python type if it is a instance or class pointer. - if self.restype is not None and issubclass(self.restype, objc_id): - result = py_from_ns(result, _auto=True) - return result - - -###################################################################### - -class ObjCPartialMethod(object): - _sentinel = object() - - def __init__(self, name_start): - super().__init__() - - self.name_start = name_start - self.methods = {} - - def __repr__(self): - return "{cls.__module__}.{cls.__qualname__}({self.name_start!r})".format(cls=type(self), self=self) - - def __call__(self, receiver, first_arg=_sentinel, **kwargs): - if first_arg is ObjCPartialMethod._sentinel: - if kwargs: - raise TypeError("Missing first (positional) argument") - - args = [] - rest = frozenset() - else: - args = [first_arg] - # Add "" to rest to indicate that the method takes arguments - rest = frozenset(kwargs) | frozenset(("",)) - - try: - meth, order = self.methods[rest] - except KeyError: - raise ValueError( - "No method was found starting with {!r} and with selector parts {}\nKnown selector parts are:\n{}" - .format(self.name_start, set(kwargs), "\n".join(repr(parts) for parts in self.methods)) - ) - - meth = ObjCMethod(meth) - args += [kwargs[name] for name in order] - return meth(receiver, *args) - - -###################################################################### - -class ObjCBoundMethod(object): - """This represents an Objective-C method (an IMP) which has been bound - to some id which will be passed as the first parameter to the method.""" - - def __init__(self, method, receiver): - """Initialize with a method and ObjCInstance or ObjCClass object.""" - self.method = method - if type(receiver) == Class: - self.receiver = cast(receiver, objc_id) - else: - self.receiver = receiver - - def __repr__(self): - return '{cls.__module__}.{cls.__qualname__}({self.method}, {self.receiver})'.format( - cls=type(self), self=self) - - def __call__(self, *args, **kwargs): - """Call the method with the given arguments.""" - return self.method(self.receiver, *args, **kwargs) - - -###################################################################### - -def cache_method(cls, name): - """Returns a python representation of the named instance method, - either by looking it up in the cached list of methods or by searching - for and creating a new method object.""" - - supercls = cls - objc_method = None - while supercls is not None: - # Load the class's methods if we haven't done so yet. - if supercls.methods_ptr is None: - supercls._load_methods() - - try: - objc_method = supercls.instance_methods[name] - break - except KeyError: - pass - - try: - objc_method = ObjCMethod(supercls.instance_method_ptrs[name]) - break - except KeyError: - pass - - supercls = supercls.superclass - - if objc_method is None: - return None - else: - cls.instance_methods[name] = objc_method - return objc_method - - -def cache_property_methods(cls, name): - """Return the accessor and mutator for the named property. - """ - if name.endswith('_'): - # If the requested name ends with _, that's a marker that we're - # dealing with a method call, not a property, so we can shortcut - # the process. - methods = None - else: - # Check 1: Does the class respond to the property? - responds = libobjc.class_getProperty(cls, name.encode('utf-8')) - - # Check 2: Does the class have an instance method to retrieve the given name - accessor = cache_method(cls, name) - - # Check 3: Is there a setName: method to set the property with the given name - mutator = cache_method(cls, 'set' + name[0].title() + name[1:] + ':') - - # Check 4: Is this a forced property on this class or a superclass? - forced = False - superclass = cls - while superclass is not None: - if name in superclass.forced_properties: - forced = True - break - superclass = superclass.superclass - - # If the class responds as a property, or it has both an accessor *and* - # and mutator, then treat it as a property in Python. - if responds or (accessor and mutator) or forced: - methods = (accessor, mutator) - else: - methods = None - return methods - - -def cache_property_accessor(cls, name): - """Returns a python representation of an accessor for the named - property. Existence of a property is done by looking for the write - selector (set:). - """ - try: - methods = cls.instance_properties[name] - except KeyError: - methods = cache_property_methods(cls, name) - cls.instance_properties[name] = methods - if methods: - return methods[0] - return None - - -def cache_property_mutator(cls, name): - """Returns a python representation of an accessor for the named - property. Existence of a property is done by looking for the write - selector (set:). - """ - try: - methods = cls.instance_properties[name] - except KeyError: - methods = cache_property_methods(cls, name) - cls.instance_properties[name] = methods - if methods: - return methods[1] - return None - - -###################################################################### - -def convert_method_arguments(encoding, args): - """Used to convert Objective-C method arguments to Python values - before passing them on to the Python-defined method. - """ - new_args = [] - for e, a in zip(encoding[3:], args): - if issubclass(e, (objc_id, ObjCInstance)): - new_args.append(py_from_ns(a, _auto=True)) - else: - new_args.append(a) - return new_args - - -def objc_method(f): - encoding = encoding_from_annotation(f) - - def _objc_method(receiver, objc_cmd, *args): - py_self = ObjCInstance(receiver) - args = convert_method_arguments(encoding, args) - result = f(py_self, *args) - if encoding[0] is not None and issubclass(encoding[0], (objc_id, ObjCInstance)): - result = ns_from_py(result) - if result is not None: - result = result.ptr - if isinstance(result, c_void_p): - return result.value - else: - return result - - def register(cls, attr): - name = attr.replace("_", ":") - cls.imp_keep_alive_table[name] = add_method(cls, name, _objc_method, encoding) - - def protocol_register(proto, attr): - name = attr.replace('_', ':') - types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding) - libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, True) - - _objc_method.register = register - _objc_method.protocol_register = protocol_register - - return _objc_method - - -def objc_classmethod(f): - encoding = encoding_from_annotation(f) - - def _objc_classmethod(objc_cls, objc_cmd, *args): - py_cls = ObjCClass(objc_cls) - args = convert_method_arguments(encoding, args) - result = f(py_cls, *args) - if encoding[0] is not None and issubclass(encoding[0], (objc_id, ObjCInstance)): - result = ns_from_py(result) - if result is not None: - result = result.ptr - if isinstance(result, c_void_p): - return result.value - else: - return result - - def register(cls, attr): - name = attr.replace("_", ":") - cls.imp_keep_alive_table[name] = add_method(cls.objc_class, name, _objc_classmethod, encoding) - - def protocol_register(proto, attr): - name = attr.replace('_', ':') - types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding) - libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, False) - - _objc_classmethod.register = register - _objc_classmethod.protocol_register = protocol_register - - return _objc_classmethod - - -class objc_ivar(object): - """Add an instance variable of type vartype to the subclass. - vartype is a ctypes type. - The class must be registered AFTER adding instance variables. - """ - def __init__(self, vartype): - self.vartype = vartype - - def pre_register(self, ptr, attr): - return add_ivar(ptr, attr, self.vartype) - - def protocol_register(self, proto, attr): - raise TypeError('Objective-C protocols cannot have ivars') - - -class objc_property(object): - """Add a property to an Objective-C class. - - An ivar, a getter and a setter are automatically generated. - If the property's type is objc_id or a subclass, the generated setter keeps the stored object retained, and - releases it when it is replaced. - """ - - def __init__(self, vartype=objc_id): - super().__init__() - - self.vartype = ctype_for_type(vartype) - - def _get_property_attributes(self): - attrs = [ - objc_property_attribute_t(b'T', encoding_for_ctype(self.vartype)), # Type: vartype - ] - if issubclass(self.vartype, objc_id): - attrs.append(objc_property_attribute_t(b'&', b'')) # retain - return (objc_property_attribute_t * len(attrs))(*attrs) - - def pre_register(self, ptr, attr): - add_ivar(ptr, '_' + attr, self.vartype) - - def register(self, cls, attr): - def _objc_getter(objc_self, _cmd): - value = get_ivar(objc_self, '_' + attr) - # ctypes complains when a callback returns a "boxed" primitive type, so we have to manually unbox it. - # If the data object has a value attribute and is not a structure or union, assume that it is - # a primitive and unbox it. - if not isinstance(value, (Structure, Union)): - try: - value = value.value - except AttributeError: - pass - - return value - - def _objc_setter(objc_self, _cmd, new_value): - if not isinstance(new_value, self.vartype): - # If vartype is a primitive, then new_value may be unboxed. If that is the case, box it manually. - new_value = self.vartype(new_value) - old_value = get_ivar(objc_self, '_' + attr) - if issubclass(self.vartype, objc_id) and new_value: - # If the new value is a non-null object, retain it. - send_message(new_value, 'retain', restype=objc_id, argtypes=[]) - set_ivar(objc_self, '_' + attr, new_value) - if issubclass(self.vartype, objc_id) and old_value: - # If the old value is a non-null object, release it. - send_message(old_value, 'release', restype=None, argtypes=[]) - - setter_name = 'set' + attr[0].upper() + attr[1:] + ':' - - cls.imp_keep_alive_table[attr] = add_method( - cls.ptr, attr, _objc_getter, - [self.vartype, ObjCInstance, SEL], - ) - cls.imp_keep_alive_table[setter_name] = add_method( - cls.ptr, setter_name, _objc_setter, - [None, ObjCInstance, SEL, self.vartype], - ) - - attrs = self._get_property_attributes() - libobjc.class_addProperty(cls, ensure_bytes(attr), attrs, len(attrs)) - - def protocol_register(self, proto, attr): - attrs = self._get_property_attributes() - libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs, len(attrs), True, True) - - -def objc_rawmethod(f): - encoding = encoding_from_annotation(f, offset=2) - - def register(cls, attr): - name = attr.replace("_", ":") - cls.imp_keep_alive_table[name] = add_method(cls, name, f, encoding) - - def protocol_register(proto, attr): - raise TypeError('Protocols cannot have method implementations, use objc_method instead of objc_rawmethod') - - f.register = register - f.protocol_register = protocol_register - - return f - - -###################################################################### - -_type_for_objcclass_map = {} - - -def type_for_objcclass(objcclass): - """Look up the ObjCInstance subclass used to represent instances of the given Objective-C class in Python. - - If the exact Objective-C class is not registered, each superclass is also checked, defaulting to ObjCInstance - if none of the classes in the superclass chain is registered. Afterwards, all searched superclasses are registered - for the ObjCInstance subclass that was found. - """ - - if isinstance(objcclass, ObjCClass): - objcclass = objcclass.ptr - - superclass = objcclass - traversed_classes = [] - pytype = ObjCInstance - while superclass.value is not None: - try: - pytype = _type_for_objcclass_map[superclass.value] - except KeyError: - traversed_classes.append(superclass) - superclass = libobjc.class_getSuperclass(superclass) - else: - break - - for cls in traversed_classes: - register_type_for_objcclass(pytype, cls) - - return pytype - - -def register_type_for_objcclass(pytype, objcclass): - """Register a conversion from an Objective-C class to an ObjCInstance subclass.""" - - if isinstance(objcclass, ObjCClass): - objcclass = objcclass.ptr - - _type_for_objcclass_map[objcclass.value] = pytype - - -def unregister_type_for_objcclass(objcclass): - """Unregister a conversion from an Objective-C class to an ObjCInstance subclass""" - - if isinstance(objcclass, ObjCClass): - objcclass = objcclass.ptr - - del _type_for_objcclass_map[objcclass.value] - - -def get_type_for_objcclass_map(): - """Get a copy of all currently registered ObjCInstance subclasses as a mapping. - Keys are Objective-C class addresses as integers. - """ - - return dict(_type_for_objcclass_map) - - -def for_objcclass(objcclass): - """Decorator for registering a conversion from an Objective-C class to an ObjCInstance subclass. - This is equivalent to calling register_type_for_objcclass. - """ - - def _for_objcclass(pytype): - register_type_for_objcclass(pytype, objcclass) - return pytype - - return _for_objcclass - - -class ObjCInstance(object): - """Python wrapper for an Objective-C instance.""" - - _cached_objects = {} - - @property - def objc_class(self): - return ObjCClass(libobjc.object_getClass(self)) - - def __new__(cls, object_ptr, _name=None, _bases=None, _ns=None): - """Create a new ObjCInstance or return a previously created one - for the given object_ptr which should be an Objective-C id.""" - # Make sure that object_ptr is wrapped in an objc_id. - if not isinstance(object_ptr, objc_id): - object_ptr = cast(object_ptr, objc_id) - - # If given a nil pointer, return None. - if not object_ptr.value: - return None - - # Check if we've already created a Python ObjCInstance for this - # object_ptr id and if so, then return it. A single ObjCInstance will - # be created for any object pointer when it is first encountered. - # This same ObjCInstance will then persist until the object is - # deallocated. - if object_ptr.value in cls._cached_objects: - return cls._cached_objects[object_ptr.value] - - # If the given pointer points to a class, return an ObjCClass instead (if we're not already creating one). - if not issubclass(cls, ObjCClass) and object_isClass(object_ptr): - return ObjCClass(object_ptr) - - # Otherwise, create a new ObjCInstance. - if issubclass(cls, type): - # Special case for ObjCClass to pass on the class name, bases and namespace to the type constructor. - self = super().__new__(cls, _name, _bases, _ns) - else: - if isinstance(object_ptr, objc_block): - cls = ObjCBlockInstance - else: - cls = type_for_objcclass(libobjc.object_getClass(object_ptr)) - self = super().__new__(cls) - super(ObjCInstance, type(self)).__setattr__(self, "ptr", object_ptr) - super(ObjCInstance, type(self)).__setattr__(self, "_as_parameter_", object_ptr) - if isinstance(object_ptr, objc_block): - super(ObjCInstance, type(self)).__setattr__(self, "block", ObjCBlock(object_ptr)) - # Store new object in the dictionary of cached objects, keyed - # by the (integer) memory address pointed to by the object_ptr. - cls._cached_objects[object_ptr.value] = self - - # Classes are never deallocated, so they don't need a DeallocationObserver. - # This is also necessary to make the definition of DeallocationObserver work - - # otherwise creating the ObjCClass for DeallocationObserver would try to - # instantiate a DeallocationObserver itself. - if not object_isClass(object_ptr): - # Create a DeallocationObserver and associate it with this object. - # When the Objective-C object is deallocated, the observer will remove - # the ObjCInstance corresponding to the object from the cached objects - # dictionary, effectively destroying the ObjCInstance. - observer = send_message( - send_message('DeallocationObserver', 'alloc', restype=objc_id, argtypes=[]), - 'initWithObject:', self, restype=objc_id, argtypes=[objc_id] - ) - libobjc.objc_setAssociatedObject(self, observer, observer, 0x301) - - # The observer is retained by the object we associate it to. We release - # the observer now so that it will be deallocated when the associated - # object is deallocated. - send_message(observer, 'release') - - return self - - def __str__(self): - desc = self.description - if desc is None: - raise ValueError('{self.name}.description returned nil'.format(self=self)) - return str(desc) - - def __repr__(self): - return "<%s.%s %#x: %s at %#x: %s>" % ( - type(self).__module__, - type(self).__qualname__, - id(self), - self.objc_class.name, - self.ptr.value, - self.debugDescription, - ) - - def __getattr__(self, name): - """Returns a callable method object with the given name.""" - # Search for named instance method in the class object and if it - # exists, return callable object with self as hidden argument. - # Note: you should give self and not self.ptr as a parameter to - # ObjCBoundMethod, so that it will be able to keep the ObjCInstance - # alive for chained calls like MyClass.alloc().init() where the - # object created by alloc() is not assigned to a variable. - - # If there's a property with this name; return the value directly. - # If the name ends with _, we can shortcut this step, because it's - # clear that we're dealing with a method call. - if not name.endswith('_'): - method = cache_property_accessor(self.objc_class, name) - if method: - return ObjCBoundMethod(method, self)() - - # See if there's a partial method starting with the given name, - # either on self's class or any of the superclasses. - cls = self.objc_class - while cls is not None: - # Load the class's methods if we haven't done so yet. - if cls.methods_ptr is None: - cls._load_methods() - - try: - method = cls.partial_methods[name] - break - except KeyError: - cls = cls.superclass - else: - method = None - - if method is not None: - # If the partial method can only resolve to one method that takes no arguments, - # return that method directly, instead of a mostly useless partial method. - if set(method.methods) == {frozenset()}: - method, _ = method.methods[frozenset()] - method = ObjCMethod(method) - - return ObjCBoundMethod(method, self) - - # See if there's a method whose full name matches the given name. - method = cache_method(self.objc_class, name.replace("_", ":")) - if method: - return ObjCBoundMethod(method, self) - else: - raise AttributeError('%s.%s %s has no attribute %s' % ( - type(self).__module__, type(self).__qualname__, self.objc_class.name, name) - ) - - def __setattr__(self, name, value): - if name in self.__dict__: - # For attributes already in __dict__, use the default __setattr__. - super(ObjCInstance, type(self)).__setattr__(self, name, value) - else: - method = cache_property_mutator(self.objc_class, name) - if method: - # Convert enums to their underlying values. - if isinstance(value, enum.Enum): - value = value.value - ObjCBoundMethod(method, self)(value) - else: - super(ObjCInstance, type(self)).__setattr__(self, name, value) - - -# The inheritance order is important here. -# type must come after ObjCInstance, so super() refers to ObjCInstance. -# This allows the ObjCInstance constructor to receive the class pointer -# as well as the name, bases, attrs arguments. -# The other way around this would not be possible, because then -# the type constructor would be called before ObjCInstance's, and there -# would be no opportunity to pass extra arguments. -class ObjCClass(ObjCInstance, type): - """Python wrapper for an Objective-C class.""" - - @property - def superclass(self): - """The superclass of this class, or None if this is a root class (such as NSObject).""" - - super_ptr = libobjc.class_getSuperclass(self) - if super_ptr.value is None: - return None - else: - return ObjCClass(super_ptr) - - @property - def protocols(self): - """The protocols adopted by this class.""" - - out_count = c_uint() - protocols_ptr = libobjc.class_copyProtocolList(self, byref(out_count)) - return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value)) - - @classmethod - def _new_from_name(cls, name): - name = ensure_bytes(name) - ptr = get_class(name) - if ptr.value is None: - raise NameError("ObjC Class '%s' couldn't be found." % name) - - return ptr, name - - @classmethod - def _new_from_ptr(cls, ptr): - ptr = cast(ptr, Class) - if ptr.value is None: - raise ValueError("Cannot create ObjCClass from nil pointer") - elif not object_isClass(ptr): - raise ValueError("Pointer {} ({:#x}) does not refer to a class".format(ptr, ptr.value)) - name = libobjc.class_getName(ptr) - - return ptr, name - - @classmethod - def _new_from_class_statement(cls, name, bases, attrs, *, protocols): - name = ensure_bytes(name) - - if get_class(name).value is not None: - raise RuntimeError('An Objective-C class named {!r} already exists'.format(name)) - - try: - (superclass,) = bases - except ValueError: - raise ValueError('An Objective-C class must have exactly one base class, not {}'.format(len(bases))) - - # Check that the superclass is an ObjCClass. - if not isinstance(superclass, ObjCClass): - raise TypeError( - 'The superclass of an Objective-C class must be an ObjCClass, ' - 'not a {cls.__module__}.{cls.__qualname__}' - .format(cls=type(superclass)) - ) - - # Check that all protocols are ObjCProtocols, and that there are no duplicates. - for proto in protocols: - if not isinstance(proto, ObjCProtocol): - raise TypeError( - 'The protocols list of an Objective-C class must contain ObjCProtocol objects, ' - 'not {cls.__module__}.{cls.__qualname__}' - .format(cls=type(proto)) - ) - elif protocols.count(proto) > 1: - raise ValueError('Protocol {} is adopted more than once'.format(proto.name)) - - # Create the ObjC class description - ptr = libobjc.objc_allocateClassPair(superclass, name, 0) - if ptr is None: - raise RuntimeError('Class pair allocation failed') - - # Adopt all the protocols. - for proto in protocols: - if not libobjc.class_addProtocol(ptr, proto): - raise RuntimeError('Failed to adopt protocol {}'.format(proto.name)) - - # Pre-Register all the instance variables - for attr, obj in attrs.items(): - if hasattr(obj, 'pre_register'): - obj.pre_register(ptr, attr) - - # Register the ObjC class - libobjc.objc_registerClassPair(ptr) - - return ptr, name, attrs - - def __new__(cls, name_or_ptr, bases=None, attrs=None, *, protocols=()): - """Create a new ObjCClass instance or return a previously created instance for the given Objective-C class. - - If called with a single class pointer argument, an ObjCClass for that class pointer is returned. - If called with a single str or bytes argument, the Objective-C with that name is returned. - - If called with three arguments, they must a name, a superclass list, and a namespace dict. A new Objective-C - class with those properties is created and returned. This form is usually called implicitly when subclassing - another ObjCClass. - In the three-argument form, an optional protocols keyword argument is also accepted. If present, it must be - a sequence of ObjCProtocol objects that the new class should adopt. - """ - - if (bases is None) ^ (attrs is None): - raise TypeError('ObjCClass arguments 2 and 3 must be given together') - - if bases is None and attrs is None: - # A single argument provided. If it's a string, treat it as - # a class name. Anything else treat as a class pointer. - - if protocols: - raise ValueError('protocols kwarg is not allowed for the single-argument form of ObjCClass') - - attrs = {} - - if isinstance(name_or_ptr, (bytes, str)): - ptr, name = cls._new_from_name(name_or_ptr) - else: - ptr, name = cls._new_from_ptr(name_or_ptr) - if not issubclass(cls, ObjCMetaClass) and libobjc.class_isMetaClass(ptr): - return ObjCMetaClass(ptr) - else: - ptr, name, attrs = cls._new_from_class_statement(name_or_ptr, bases, attrs, protocols=protocols) - - objc_class_name = name.decode('utf-8') - - new_attrs = { - '_class_inited': False, - 'name': objc_class_name, - 'methods_ptr_count': c_uint(0), - 'methods_ptr': None, - # Mapping of name -> method pointer - 'instance_method_ptrs': {}, - # Mapping of name -> instance method - 'instance_methods': {}, - # Mapping of name -> (accessor method, mutator method) - 'instance_properties': {}, - # Explicitly declared properties - 'forced_properties': set(), - # Mapping of first selector part -> ObjCPartialMethod instances - 'partial_methods': {}, - # Mapping of name -> CFUNCTYPE callback function - # This only contains the IMPs of methods created in Python, - # which need to be kept from being garbage-collected. - # It does not contain any other methods, do not use it for calling methods. - 'imp_keep_alive_table': {}, - } - - # On Python 3.6 and later, the class namespace may contain a __classcell__ attribute that must be passed on - # to type.__new__. See https://docs.python.org/3/reference/datamodel.html#creating-the-class-object - if '__classcell__' in attrs: - new_attrs['__classcell__'] = attrs['__classcell__'] - - # Create the class object. If there is already a cached instance for ptr, - # it is returned and the additional arguments are ignored. - # Logically this can only happen when creating an ObjCClass from an existing - # name or pointer, not when creating a new class. - # If there is no cached instance for ptr, a new one is created and cached. - self = super().__new__(cls, ptr, objc_class_name, (ObjCInstance,), new_attrs) - - if not self._class_inited: - self._class_inited = True - - # Register all the methods, class methods, etc - registered_something = False - for attr, obj in attrs.items(): - if hasattr(obj, "register"): - registered_something = True - obj.register(self, attr) - - # If anything was registered, reload the methods of this class - # (and the metaclass, because there may be new class methods). - if registered_something: - self._load_methods() - self.objc_class._load_methods() - - return self - - def __init__(self, *args, **kwargs): - # Prevent kwargs from being passed on to type.__init__, which does not accept any kwargs in Python < 3.6. - super().__init__(*args) - - def declare_property(self, name): - self.forced_properties.add(name) - - def declare_class_property(self, name): - self.objc_class.forced_properties.add(name) - - def __repr__(self): - return "<%s.%s: %s at %#x>" % ( - type(self).__module__, - type(self).__qualname__, - self.name, - self.ptr.value, - ) - - def __str__(self): - return "{cls.__name__}({self.name!r})".format(cls=type(self), self=self) - - def __del__(self): - libc.free(self.methods_ptr) - - def __instancecheck__(self, instance): - if isinstance(instance, ObjCInstance): - return bool(instance.isKindOfClass(self)) - else: - return False - - def __subclasscheck__(self, subclass): - if isinstance(subclass, ObjCClass): - return bool(subclass.isSubclassOfClass(self)) - else: - raise TypeError( - 'issubclass(X, {self!r}) arg 1 must be an ObjCClass, not {tp.__module__}.{tp.__qualname__}' - .format(self=self, tp=type(subclass)) - ) - - def _load_methods(self): - if self.methods_ptr is not None: - raise RuntimeError("_load_methods cannot be called more than once") - - self.methods_ptr = libobjc.class_copyMethodList(self, byref(self.methods_ptr_count)) - - if self.superclass is not None and self.superclass.methods_ptr is None: - self.superclass._load_methods() - - for i in range(self.methods_ptr_count.value): - method = self.methods_ptr[i] - name = libobjc.method_getName(method).name.decode("utf-8") - self.instance_method_ptrs[name] = method - - first, *rest = name.split(":") - # Selectors end in a colon iff the method takes arguments. - # Because of this, rest must either be empty (method takes no arguments) - # or the last element must be an empty string (method takes arguments). - assert not rest or rest[-1] == "" - - try: - partial = self.partial_methods[first] - except KeyError: - if self.superclass is None: - super_partial = None - else: - super_partial = self.superclass.partial_methods.get(first) - - partial = self.partial_methods[first] = ObjCPartialMethod(first) - if super_partial is not None: - partial.methods.update(super_partial.methods) - - # order is rest without the dummy "" part - order = rest[:-1] - partial.methods[frozenset(rest)] = (method, order) - - -class ObjCMetaClass(ObjCClass): - """Python wrapper for an Objective-C metaclass.""" - - def __new__(cls, name_or_ptr): - if isinstance(name_or_ptr, (bytes, str)): - name = ensure_bytes(name_or_ptr) - ptr = libobjc.objc_getMetaClass(name) - if ptr.value is None: - raise NameError("Objective-C metaclass {} not found".format(name)) - else: - ptr = cast(name_or_ptr, Class) - if ptr.value is None: - raise ValueError("Cannot create ObjCMetaClass for nil pointer") - elif not object_isClass(ptr) or not libobjc.class_isMetaClass(ptr): - raise ValueError("Pointer {} ({:#x}) does not refer to a metaclass".format(ptr, ptr.value)) - - return super().__new__(cls, ptr) - - -register_ctype_for_type(ObjCInstance, objc_id) -register_ctype_for_type(ObjCClass, Class) - - -NSObject = ObjCClass('NSObject') -NSObject.declare_property('debugDescription') -NSObject.declare_property('description') -NSNumber = ObjCClass('NSNumber') -NSDecimalNumber = ObjCClass('NSDecimalNumber') -NSString = ObjCClass('NSString') -NSString.declare_property('UTF8String') -NSData = ObjCClass('NSData') -NSArray = ObjCClass('NSArray') -NSMutableArray = ObjCClass('NSMutableArray') -NSDictionary = ObjCClass('NSDictionary') -NSMutableDictionary = ObjCClass('NSMutableDictionary') -Protocol = ObjCClass('Protocol') - - -def py_from_ns(nsobj, *, _auto=False): - """Convert a Foundation object into an equivalent Python object if possible. - - Currently supported types: - - * ``objc_id``, ``Class``: Wrapped in an ``ObjCInstance`` and converted as below - * ``NSString``: Converted to ``str`` - * ``NSData``: Converted to ``bytes`` - * ``NSDecimalNumber``: Converted to ``decimal.Decimal`` - * ``NSDictionary``: Converted to ``dict``, with all keys and values converted recursively - * ``NSArray``: Converted to ``list``, with all elements converted recursively - * ``NSNumber``: Converted to a ``bool``, ``int`` or ``float`` based on the type of its contents - - Other objects are returned unmodified as an ``ObjCInstance``. - """ - - if isinstance(nsobj, (objc_id, Class)): - nsobj = ObjCInstance(nsobj) - if not isinstance(nsobj, ObjCInstance): - return nsobj - - if nsobj.isKindOfClass(NSDecimalNumber): - return decimal.Decimal(str(nsobj.descriptionWithLocale(None))) - elif nsobj.isKindOfClass(NSNumber): - # Choose the property to access based on the type encoding. The actual conversion is done by ctypes. - # Signed and unsigned integers are in separate cases to prevent overflow with unsigned long longs. - objc_type = nsobj.objCType - if objc_type == b'B': - return nsobj.boolValue - elif objc_type in b'csilq': - return nsobj.longLongValue - elif objc_type in b'CSILQ': - return nsobj.unsignedLongLongValue - elif objc_type in b'fd': - return nsobj.doubleValue - else: - raise TypeError( - 'NSNumber containing unsupported type {!r} cannot be converted to a Python object' - .format(objc_type) - ) - elif _auto: - # If py_from_ns is called implicitly to convert an Objective-C method's return value, only the conversions - # before this branch are performed. If py_from_ns is called explicitly by hand, the additional conversions - # below this branch are performed as well. - # _auto is a private kwarg that is only passed when py_from_ns is called implicitly. In that case, we return - # early and don't attempt any other conversions. - return nsobj - elif nsobj.isKindOfClass(NSString): - return str(nsobj) - elif nsobj.isKindOfClass(NSData): - # Despite the name, string_at converts the data at the address to a bytes object, not str. - return string_at(send_message(nsobj, 'bytes', restype=POINTER(c_uint8), argtypes=[]), nsobj.length) - elif nsobj.isKindOfClass(NSDictionary): - return {py_from_ns(k): py_from_ns(v) for k, v in nsobj.items()} - elif nsobj.isKindOfClass(NSArray): - return [py_from_ns(o) for o in nsobj] - else: - return nsobj - - -def ns_from_py(pyobj): - """Convert a Python object into an equivalent Foundation object. The returned object is autoreleased. - - This function is also available under the name ``at``, because its functionality is very similar to that of the - Objective-C ``@`` operator and literals. - - Currently supported types: - - * ``None``, ``ObjCInstance``: Returned as-is - * ``enum.Enum``: Replaced by their ``value`` and converted as below - * ``str``: Converted to ``NSString`` - * ``bytes``: Converted to ``NSData`` - * ``decimal.Decimal``: Converted to ``NSDecimalNumber`` - * ``dict``: Converted to ``NSDictionary``, with all keys and values converted recursively - * ``list``: Converted to ``NSArray``, with all elements converted recursively - * ``bool``, ``int``, ``float``: Converted to ``NSNumber`` - - Other types cause a ``TypeError``. - """ - - if isinstance(pyobj, enum.Enum): - pyobj = pyobj.value - - # Many Objective-C method calls here use the convert_result=False kwarg to disable automatic conversion of - # return values, because otherwise most of the Objective-C objects would be converted back to Python objects. - if pyobj is None or isinstance(pyobj, ObjCInstance): - return pyobj - elif isinstance(pyobj, str): - return ObjCInstance(NSString.stringWithUTF8String_(pyobj.encode('utf-8'), convert_result=False)) - elif isinstance(pyobj, bytes): - return ObjCInstance(NSData.dataWithBytes(pyobj, length=len(pyobj))) - elif isinstance(pyobj, decimal.Decimal): - return ObjCInstance(NSDecimalNumber.decimalNumberWithString_(pyobj.to_eng_string(), convert_result=False)) - elif isinstance(pyobj, dict): - dikt = NSMutableDictionary.dictionaryWithCapacity(len(pyobj)) - for k, v in pyobj.items(): - dikt.setObject(v, forKey=k) - return dikt - elif isinstance(pyobj, list): - array = NSMutableArray.arrayWithCapacity(len(pyobj)) - for v in pyobj: - array.addObject(v) - return array - elif isinstance(pyobj, bool): - return ObjCInstance(NSNumber.numberWithBool_(pyobj, convert_result=False)) - elif isinstance(pyobj, int): - return ObjCInstance(NSNumber.numberWithLong_(pyobj, convert_result=False)) - elif isinstance(pyobj, float): - return ObjCInstance(NSNumber.numberWithDouble_(pyobj, convert_result=False)) - else: - raise TypeError( - "Don't know how to convert a {cls.__module__}.{cls.__qualname__} to a Foundation object" - .format(cls=type(pyobj)) - ) - - -at = ns_from_py - - -@for_objcclass(Protocol) -class ObjCProtocol(ObjCInstance): - """Python wrapper for an Objective-C protocol.""" - - @property - def name(self): - """The name of this protocol.""" - - return libobjc.protocol_getName(self).decode('utf-8') - - @property - def protocols(self): - """The superprotocols of this protocol.""" - - out_count = c_uint() - protocols_ptr = libobjc.protocol_copyProtocolList(self, byref(out_count)) - return tuple(ObjCProtocol(protocols_ptr[i]) for i in range(out_count.value)) - - def __new__(cls, name_or_ptr, bases=None, ns=None): - if (bases is None) ^ (ns is None): - raise TypeError('ObjCProtocol arguments 2 and 3 must be given together') - - if bases is None and ns is None: - if isinstance(name_or_ptr, (bytes, str)): - name = ensure_bytes(name_or_ptr) - ptr = libobjc.objc_getProtocol(name) - if ptr.value is None: - raise NameError('Objective-C protocol {} not found'.format(name)) - else: - ptr = cast(name_or_ptr, objc_id) - if ptr.value is None: - raise ValueError('Cannot create ObjCProtocol for nil pointer') - elif not send_message(ptr, 'isKindOfClass:', Protocol, restype=c_bool, argtypes=[objc_id]): - raise ValueError('Pointer {} ({:#x}) does not refer to a protocol'.format(ptr, ptr.value)) - else: - name = ensure_bytes(name_or_ptr) - - if libobjc.objc_getProtocol(name).value is not None: - raise RuntimeError('An Objective-C protocol named {!r} already exists'.format(name)) - - # Check that all bases are protocols. - for base in bases: - if not isinstance(base, ObjCProtocol): - raise TypeError( - 'An Objective-C protocol can only extend ObjCProtocol objects, ' - 'not {cls.__module__}.{cls.__qualname__}' - .format(cls=type(base)) - ) - - # Allocate the protocol object. - ptr = libobjc.objc_allocateProtocol(name) - if ptr is None: - raise RuntimeError('Protocol allocation failed') - - # Adopt all the protocols. - for proto in bases: - libobjc.protocol_addProtocol(ptr, proto) - - # Register all methods and properties. - for attr, obj in ns.items(): - if hasattr(obj, 'protocol_register'): - obj.protocol_register(ptr, attr) - - # Register the protocol object - libobjc.objc_registerProtocol(ptr) - - return super().__new__(cls, ptr) - - def __repr__(self): - return '<{cls.__module__}.{cls.__qualname__}: {self.name} at {self.ptr.value:#x}>'.format( - cls=type(self), self=self) - - def __instancecheck__(self, instance): - if isinstance(instance, ObjCInstance): - return bool(instance.conformsToProtocol(self)) - else: - return False - - def __subclasscheck__(self, subclass): - if isinstance(subclass, ObjCClass): - return bool(subclass.conformsToProtocol(self)) - elif isinstance(subclass, ObjCProtocol): - return bool(libobjc.protocol_conformsToProtocol(subclass, self)) - else: - raise TypeError( - 'issubclass(X, {self!r}) arg 1 must be an ObjCClass or ObjCProtocol, ' - 'not {tp.__module__}.{tp.__qualname__}' - .format(self=self, tp=type(subclass)) - ) - - -# Need to use a different name to avoid conflict with the NSObject class. -# NSObjectProtocol is also the name that Swift uses when importing the NSObject protocol. -NSObjectProtocol = ObjCProtocol('NSObject') - - -###################################################################### - -# Instances of DeallocationObserver are associated with every -# Objective-C object that gets wrapped inside an ObjCInstance. -# Their sole purpose is to watch for when the Objective-C object -# is deallocated, and then remove the object from the dictionary -# of cached ObjCInstance objects kept by the ObjCInstance class. -# -# The methods of the class defined below are decorated with -# rawmethod() instead of method() because DeallocationObservers -# are created inside of ObjCInstance's __new__ method and we have -# to be careful to not create another ObjCInstance here (which -# happens when the usual method decorator turns the self argument -# into an ObjCInstance), or else get trapped in an infinite recursion. - -# Try to reuse an existing DeallocationObserver class. -# This allows reloading the module without having to restart -# the interpreter, although any changes to DeallocationObserver -# itself are only applied after a restart of course. -try: - DeallocationObserver = ObjCClass("DeallocationObserver") -except NameError: - class DeallocationObserver(NSObject): - - observed_object = objc_ivar(objc_id) - - @objc_rawmethod - def initWithObject_(self, cmd, anObject): - self = send_message(self, 'init', restype=objc_id, argtypes=[]) - if self is not None: - set_ivar(self, 'observed_object', anObject) - return self.value - - @objc_rawmethod - def dealloc(self, cmd) -> None: - anObject = get_ivar(self, 'observed_object') - ObjCInstance._cached_objects.pop(anObject.value, None) - send_super(__class__, self, 'dealloc', restype=None, argtypes=[]) - - @objc_rawmethod - def finalize(self, cmd) -> None: - # Called instead of dealloc if using garbage collection. - # (which would have to be explicitly started with - # objc_startCollectorThread(), so probably not too much reason - # to have this here, but I guess it can't hurt.) - anObject = get_ivar(self, 'observed_object') - ObjCInstance._cached_objects.pop(anObject.value, None) - send_super(__class__, self, 'finalize', restype=None, argtypes=[]) - - -def objc_const(dll, name): - """Create an ObjCInstance from a global pointer variable in a DLL.""" - - return ObjCInstance(objc_id.in_dll(dll, name)) - - -_cfunc_type_block_invoke = CFUNCTYPE(c_void_p, c_void_p) -_cfunc_type_block_dispose = CFUNCTYPE(c_void_p, c_void_p) -_cfunc_type_block_copy = CFUNCTYPE(c_void_p, c_void_p, c_void_p) - - -class ObjCBlockStruct(Structure): - _fields_ = [ - ('isa', c_void_p), - ('flags', c_int), - ('reserved', c_int), - ('invoke', _cfunc_type_block_invoke), - ('descriptor', c_void_p), - ] - - -class BlockDescriptor(Structure): - _fields_ = [ - ('reserved', c_ulong), - ('size', c_ulong), - ('copy_helper', _cfunc_type_block_copy), - ('dispose_helper', _cfunc_type_block_dispose), - ('signature', c_char_p), - ] - - -class BlockLiteral(Structure): - _fields_ = [ - ('isa', c_void_p), - ('flags', c_int), - ('reserved', c_int), - ('invoke', c_void_p), # NB: this must be c_void_p due to variadic nature - ('descriptor', c_void_p) - ] - - -def create_block_descriptor_struct(has_helpers, has_signature): - descriptor_fields = [ - ('reserved', c_ulong), - ('size', c_ulong), - ] - if has_helpers: - descriptor_fields.extend([ - ('copy_helper', _cfunc_type_block_copy), - ('dispose_helper', _cfunc_type_block_dispose), - ]) - if has_signature: - descriptor_fields.extend([ - ('signature', c_char_p), - ]) - return type( - 'ObjCBlockDescriptor', - (Structure, ), - {'_fields_': descriptor_fields} - ) - - -def cast_block_descriptor(block): - descriptor_struct = create_block_descriptor_struct(block.has_helpers, block.has_signature) - return cast(block.struct.contents.descriptor, POINTER(descriptor_struct)) - - -AUTO = object() - - -class BlockConsts: - HAS_COPY_DISPOSE = 1 << 25 - HAS_CTOR = 1 << 26 - IS_GLOBAL = 1 << 28 - HAS_STRET = 1 << 29 - HAS_SIGNATURE = 1 << 30 - - -class ObjCBlock: - def __init__(self, pointer, return_type=AUTO, *arg_types): - if isinstance(pointer, ObjCInstance): - pointer = pointer.ptr - self.pointer = pointer - self.struct = cast(self.pointer, POINTER(ObjCBlockStruct)) - self.has_helpers = self.struct.contents.flags & BlockConsts.HAS_COPY_DISPOSE - self.has_signature = self.struct.contents.flags & BlockConsts.HAS_SIGNATURE - self.descriptor = cast_block_descriptor(self) - self.signature = self.descriptor.contents.signature if self.has_signature else None - if return_type is AUTO: - if arg_types: - raise ValueError('Cannot use arg_types with return_type AUTO') - if not self.has_signature: - raise ValueError('Cannot use AUTO types for blocks without signatures') - return_type, *arg_types = ctypes_for_method_encoding(self.signature) - self.struct.contents.invoke.restype = ctype_for_type(return_type) - self.struct.contents.invoke.argtypes = (objc_id, ) + tuple(ctype_for_type(arg_type) for arg_type in arg_types) - - def __repr__(self): - representation = ' Date: Tue, 29 May 2018 21:59:35 +0200 Subject: [PATCH 11/30] Remove unused get_metaclass and get_superclass_of_object functions --- rubicon/objc/runtime.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index ceb4b0b2..960c3764 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -22,8 +22,6 @@ 'c_ptrdiff_t', 'get_class', 'get_ivar', - 'get_metaclass', - 'get_superclass_of_object', 'libc', 'libobjc', 'objc_block', @@ -512,17 +510,6 @@ def get_class(name): return libobjc.objc_getClass(ensure_bytes(name)) -def get_metaclass(name): - "Return a reference to the metaclass for the given name." - return libobjc.objc_getMetaClass(ensure_bytes(name)) - - -def get_superclass_of_object(obj): - "Return a reference to the superclass of the given object." - cls = libobjc.object_getClass(obj) - return libobjc.class_getSuperclass(cls) - - # http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html # http://www.x86-64.org/documentation/abi-0.99.pdf (pp.17-23) # executive summary: on x86-64, who knows? From bdfc15146ee3e677ca9791a37a6710ab5b5411d3 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 29 May 2018 22:09:19 +0200 Subject: [PATCH 12/30] Update rubicon.objc.api.__all__ Added some previously missing names (various ObjCClass and ObjCProtocol objects), and removed some names that are for internal use only and shouldn't be public. --- rubicon/objc/api.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/rubicon/objc/api.py b/rubicon/objc/api.py index 44c4ac40..33a5b25d 100644 --- a/rubicon/objc/api.py +++ b/rubicon/objc/api.py @@ -18,30 +18,24 @@ __all__ = [ 'Block', - 'BlockConsts', - 'BlockDescriptor', - 'BlockLiteral', - 'DeallocationObserver', + 'NSArray', + 'NSData', + 'NSDecimalNumber', + 'NSDictionary', + 'NSMutableArray', + 'NSMutableDictionary', + 'NSNumber', 'NSObject', + 'NSObjectProtocol', + 'NSString', 'ObjCBlock', - 'ObjCBlockInstance', - 'ObjCBlockStruct', - 'ObjCBoundMethod', 'ObjCClass', 'ObjCInstance', 'ObjCMetaClass', 'ObjCMethod', - 'ObjCPartialMethod', 'ObjCProtocol', + 'Protocol', 'at', - 'cache_method', - 'cache_property_accessor', - 'cache_property_methods', - 'cache_property_mutator', - 'cast_block_descriptor', - 'convert_method_arguments', - 'create_block_descriptor_struct', - 'encoding_from_annotation', 'for_objcclass', 'get_type_for_objcclass_map', 'ns_from_py', From e9ccf357d31fba0a5fcb2a9473152b8f388580ae Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 29 May 2018 22:11:43 +0200 Subject: [PATCH 13/30] Add explicit import of .api to __init__ --- rubicon/objc/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rubicon/objc/__init__.py b/rubicon/objc/__init__.py index 8692e19c..549d0116 100644 --- a/rubicon/objc/__init__.py +++ b/rubicon/objc/__init__.py @@ -1,10 +1,11 @@ __version__ = '0.2.10' # Import commonly used submodules right away. -# The first two imports are only included for clarity. They are not strictly necessary, because the from-imports below +# The first few imports are only included for clarity. They are not strictly necessary, because the from-imports below # also import the types and runtime modules and implicitly add them to the rubicon.objc namespace. from . import types # noqa: F401 from . import runtime # noqa: F401 +from . import api # noqa: F401 # The import of collections is important, however. The classes from collections are not meant to be used directly, # instead they are registered with the runtime module (using the for_objcclass decorator) so they are used in place of # ObjCInstance when representing Foundation collections in Python. If this module is not imported, the registration From ed95f9b439ad8747d2fed1058245f498307f1814 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 29 May 2018 22:16:36 +0200 Subject: [PATCH 14/30] Move from .types import into a more logical position in __init__ --- rubicon/objc/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rubicon/objc/__init__.py b/rubicon/objc/__init__.py index 549d0116..317749a0 100644 --- a/rubicon/objc/__init__.py +++ b/rubicon/objc/__init__.py @@ -12,6 +12,11 @@ # will not take place, and Foundation collections will not support the expected methods/operators in Python! from . import collections # noqa: F401 +from .types import ( # noqa: F401 + CFIndex, CFRange, CGFloat, CGGlyph, CGPoint, CGPointMake, CGRect, CGRectMake, CGSize, CGSizeMake, NSEdgeInsets, + NSEdgeInsetsMake, NSInteger, NSMakePoint, NSMakeRect, NSMakeSize, NSPoint, NSRange, NSRect, NSSize, NSTimeInterval, + NSUInteger, NSZeroPoint, UIEdgeInsets, UIEdgeInsetsMake, UIEdgeInsetsZero, UniChar, unichar, +) from .runtime import ( # noqa: F401 IMP, SEL, Class, Ivar, Method, get_ivar, objc_id, objc_property_t, send_message, send_super, set_ivar, ) @@ -20,10 +25,3 @@ ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, ns_from_py, objc_classmethod, objc_const, objc_ivar, objc_method, objc_property, objc_rawmethod, py_from_ns, ) -from .types import ( # noqa: F401 - CFIndex, CFRange, CGFloat, CGGlyph, CGPoint, CGPointMake, CGRect, - CGRectMake, CGSize, CGSizeMake, NSEdgeInsets, NSEdgeInsetsMake, NSInteger, - NSMakePoint, NSMakeRect, NSMakeSize, NSPoint, NSRange, NSRect, NSSize, - NSTimeInterval, NSUInteger, NSZeroPoint, UIEdgeInsets, UIEdgeInsetsMake, - UIEdgeInsetsZero, UniChar, unichar, -) From 5e2fa06523cdfa79cbacf0c6fef24a6ecc0e6f27 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 29 May 2018 22:30:43 +0200 Subject: [PATCH 15/30] Convert the cache functions in .api to ObjCClass methods The functions only accept an ObjCClass as the first argument, and they are required for ObjCInstance/ObjCClass to function properly, so it makes more sense to have them as methods. --- rubicon/objc/api.py | 200 ++++++++++++++++++++++---------------------- 1 file changed, 98 insertions(+), 102 deletions(-) diff --git a/rubicon/objc/api.py b/rubicon/objc/api.py index 33a5b25d..2d5facba 100644 --- a/rubicon/objc/api.py +++ b/rubicon/objc/api.py @@ -206,105 +206,6 @@ def __call__(self, *args, **kwargs): return self.method(self.receiver, *args, **kwargs) -def cache_method(cls, name): - """Returns a python representation of the named instance method, - either by looking it up in the cached list of methods or by searching - for and creating a new method object.""" - - supercls = cls - objc_method = None - while supercls is not None: - # Load the class's methods if we haven't done so yet. - if supercls.methods_ptr is None: - supercls._load_methods() - - try: - objc_method = supercls.instance_methods[name] - break - except KeyError: - pass - - try: - objc_method = ObjCMethod(supercls.instance_method_ptrs[name]) - break - except KeyError: - pass - - supercls = supercls.superclass - - if objc_method is None: - return None - else: - cls.instance_methods[name] = objc_method - return objc_method - - -def cache_property_methods(cls, name): - """Return the accessor and mutator for the named property. - """ - if name.endswith('_'): - # If the requested name ends with _, that's a marker that we're - # dealing with a method call, not a property, so we can shortcut - # the process. - methods = None - else: - # Check 1: Does the class respond to the property? - responds = libobjc.class_getProperty(cls, name.encode('utf-8')) - - # Check 2: Does the class have an instance method to retrieve the given name - accessor = cache_method(cls, name) - - # Check 3: Is there a setName: method to set the property with the given name - mutator = cache_method(cls, 'set' + name[0].title() + name[1:] + ':') - - # Check 4: Is this a forced property on this class or a superclass? - forced = False - superclass = cls - while superclass is not None: - if name in superclass.forced_properties: - forced = True - break - superclass = superclass.superclass - - # If the class responds as a property, or it has both an accessor *and* - # and mutator, then treat it as a property in Python. - if responds or (accessor and mutator) or forced: - methods = (accessor, mutator) - else: - methods = None - return methods - - -def cache_property_accessor(cls, name): - """Returns a python representation of an accessor for the named - property. Existence of a property is done by looking for the write - selector (set:). - """ - try: - methods = cls.instance_properties[name] - except KeyError: - methods = cache_property_methods(cls, name) - cls.instance_properties[name] = methods - if methods: - return methods[0] - return None - - -def cache_property_mutator(cls, name): - """Returns a python representation of an accessor for the named - property. Existence of a property is done by looking for the write - selector (set:). - """ - try: - methods = cls.instance_properties[name] - except KeyError: - methods = cache_property_methods(cls, name) - cls.instance_properties[name] = methods - if methods: - return methods[1] - return None - - def convert_method_arguments(encoding, args): """Used to convert Objective-C method arguments to Python values before passing them on to the Python-defined method. @@ -652,7 +553,7 @@ def __getattr__(self, name): # If the name ends with _, we can shortcut this step, because it's # clear that we're dealing with a method call. if not name.endswith('_'): - method = cache_property_accessor(self.objc_class, name) + method = self.objc_class._cache_property_accessor(name) if method: return ObjCBoundMethod(method, self)() @@ -682,7 +583,7 @@ def __getattr__(self, name): return ObjCBoundMethod(method, self) # See if there's a method whose full name matches the given name. - method = cache_method(self.objc_class, name.replace("_", ":")) + method = self.objc_class._cache_method(name.replace("_", ":")) if method: return ObjCBoundMethod(method, self) else: @@ -695,7 +596,7 @@ def __setattr__(self, name, value): # For attributes already in __dict__, use the default __setattr__. super(ObjCInstance, type(self)).__setattr__(self, name, value) else: - method = cache_property_mutator(self.objc_class, name) + method = self.objc_class._cache_property_mutator(name) if method: # Convert enums to their underlying values. if isinstance(value, enum.Enum): @@ -896,6 +797,101 @@ def __init__(self, *args, **kwargs): # Prevent kwargs from being passed on to type.__init__, which does not accept any kwargs in Python < 3.6. super().__init__(*args) + def _cache_method(self, name): + """Returns a python representation of the named instance method, + either by looking it up in the cached list of methods or by searching + for and creating a new method object.""" + + supercls = self + objc_method = None + while supercls is not None: + # Load the class's methods if we haven't done so yet. + if supercls.methods_ptr is None: + supercls._load_methods() + + try: + objc_method = supercls.instance_methods[name] + break + except KeyError: + pass + + try: + objc_method = ObjCMethod(supercls.instance_method_ptrs[name]) + break + except KeyError: + pass + + supercls = supercls.superclass + + if objc_method is None: + return None + else: + self.instance_methods[name] = objc_method + return objc_method + + def _cache_property_methods(self, name): + """Return the accessor and mutator for the named property. + """ + if name.endswith('_'): + # If the requested name ends with _, that's a marker that we're + # dealing with a method call, not a property, so we can shortcut + # the process. + methods = None + else: + # Check 1: Does the class respond to the property? + responds = libobjc.class_getProperty(self, name.encode('utf-8')) + + # Check 2: Does the class have an instance method to retrieve the given name + accessor = self._cache_method(name) + + # Check 3: Is there a setName: method to set the property with the given name + mutator = self._cache_method('set' + name[0].title() + name[1:] + ':') + + # Check 4: Is this a forced property on this class or a superclass? + forced = False + superclass = self + while superclass is not None: + if name in superclass.forced_properties: + forced = True + break + superclass = superclass.superclass + + # If the class responds as a property, or it has both an accessor *and* + # and mutator, then treat it as a property in Python. + if responds or (accessor and mutator) or forced: + methods = (accessor, mutator) + else: + methods = None + return methods + + def _cache_property_accessor(self, name): + """Returns a python representation of an accessor for the named + property. Existence of a property is done by looking for the write + selector (set:). + """ + try: + methods = self.instance_properties[name] + except KeyError: + methods = self._cache_property_methods(name) + self.instance_properties[name] = methods + if methods: + return methods[0] + return None + + def _cache_property_mutator(self, name): + """Returns a python representation of an accessor for the named + property. Existence of a property is done by looking for the write + selector (set:). + """ + try: + methods = self.instance_properties[name] + except KeyError: + methods = self._cache_property_methods(name) + self.instance_properties[name] = methods + if methods: + return methods[1] + return None + def declare_property(self, name): self.forced_properties.add(name) From 36950f01120b9cf97159028bee6c92bc9b9d470a Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 29 May 2018 22:42:04 +0200 Subject: [PATCH 16/30] Convert objc_method and related decorators to classes This makes the behavior of the register and protocol_register attributes/methods clearer, and matches the style used for objc_ivar and objc_property. --- rubicon/objc/api.py | 82 ++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/rubicon/objc/api.py b/rubicon/objc/api.py index 2d5facba..273714b9 100644 --- a/rubicon/objc/api.py +++ b/rubicon/objc/api.py @@ -219,14 +219,18 @@ def convert_method_arguments(encoding, args): return new_args -def objc_method(f): - encoding = encoding_from_annotation(f) - - def _objc_method(receiver, objc_cmd, *args): - py_self = ObjCInstance(receiver) - args = convert_method_arguments(encoding, args) - result = f(py_self, *args) - if encoding[0] is not None and issubclass(encoding[0], (objc_id, ObjCInstance)): +class objc_method(object): + def __init__(self, py_method): + super().__init__() + + self.py_method = py_method + self.encoding = encoding_from_annotation(py_method) + + def __call__(self, objc_self, objc_cmd, *args): + py_self = ObjCInstance(objc_self) + args = convert_method_arguments(self.encoding, args) + result = self.py_method(py_self, *args) + if self.encoding[0] is not None and issubclass(self.encoding[0], (objc_id, ObjCInstance)): result = ns_from_py(result) if result is not None: result = result.ptr @@ -235,29 +239,28 @@ def _objc_method(receiver, objc_cmd, *args): else: return result - def register(cls, attr): + def register(self, cls, attr): name = attr.replace("_", ":") - cls.imp_keep_alive_table[name] = add_method(cls, name, _objc_method, encoding) + cls.imp_keep_alive_table[name] = add_method(cls, name, self, self.encoding) - def protocol_register(proto, attr): + def protocol_register(self, proto, attr): name = attr.replace('_', ':') - types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding) + types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in self.encoding) libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, True) - _objc_method.register = register - _objc_method.protocol_register = protocol_register - - return _objc_method +class objc_classmethod(object): + def __init__(self, py_method): + super().__init__() -def objc_classmethod(f): - encoding = encoding_from_annotation(f) + self.py_method = py_method + self.encoding = encoding_from_annotation(py_method) - def _objc_classmethod(objc_cls, objc_cmd, *args): + def __call__(self, objc_cls, objc_cmd, *args): py_cls = ObjCClass(objc_cls) - args = convert_method_arguments(encoding, args) - result = f(py_cls, *args) - if encoding[0] is not None and issubclass(encoding[0], (objc_id, ObjCInstance)): + args = convert_method_arguments(self.encoding, args) + result = self.py_method(py_cls, *args) + if self.encoding[0] is not None and issubclass(self.encoding[0], (objc_id, ObjCInstance)): result = ns_from_py(result) if result is not None: result = result.ptr @@ -266,20 +269,15 @@ def _objc_classmethod(objc_cls, objc_cmd, *args): else: return result - def register(cls, attr): + def register(self, cls, attr): name = attr.replace("_", ":") - cls.imp_keep_alive_table[name] = add_method(cls.objc_class, name, _objc_classmethod, encoding) + cls.imp_keep_alive_table[name] = add_method(cls.objc_class, name, self, self.encoding) - def protocol_register(proto, attr): + def protocol_register(self, proto, attr): name = attr.replace('_', ':') - types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in encoding) + types = b''.join(encoding_for_ctype(ctype_for_type(tp)) for tp in self.encoding) libobjc.protocol_addMethodDescription(proto, SEL(name), types, True, False) - _objc_classmethod.register = register - _objc_classmethod.protocol_register = protocol_register - - return _objc_classmethod - class objc_ivar(object): """Add an instance variable of type vartype to the subclass. @@ -366,20 +364,22 @@ def protocol_register(self, proto, attr): libobjc.protocol_addProperty(proto, ensure_bytes(attr), attrs, len(attrs), True, True) -def objc_rawmethod(f): - encoding = encoding_from_annotation(f, offset=2) +class objc_rawmethod(object): + def __init__(self, py_method): + super().__init__() - def register(cls, attr): - name = attr.replace("_", ":") - cls.imp_keep_alive_table[name] = add_method(cls, name, f, encoding) + self.py_method = py_method + self.encoding = encoding_from_annotation(py_method, offset=2) - def protocol_register(proto, attr): - raise TypeError('Protocols cannot have method implementations, use objc_method instead of objc_rawmethod') + def __call__(self, *args, **kwargs): + return self.py_method(*args, **kwargs) - f.register = register - f.protocol_register = protocol_register + def register(self, cls, attr): + name = attr.replace("_", ":") + cls.imp_keep_alive_table[name] = add_method(cls, name, self, self.encoding) - return f + def protocol_register(self, proto, attr): + raise TypeError('Protocols cannot have method implementations, use objc_method instead of objc_rawmethod') _type_for_objcclass_map = {} From 387a3f2f75a5384e5b0cfa933860eb2d14a6a24c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 30 May 2018 10:40:14 +0800 Subject: [PATCH 17/30] Refactored tests to compare python and objc behavior directly. --- tests/test_NSString.py | 273 +++++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 136 deletions(-) diff --git a/tests/test_NSString.py b/tests/test_NSString.py index 09b0817d..9ce48bbc 100644 --- a/tests/test_NSString.py +++ b/tests/test_NSString.py @@ -28,6 +28,49 @@ class NSStringTests(unittest.TestCase): NEEDLES = ['', 'a', 'bcd', 'def', HAYSTACK, 'nope', 'dcb'] RANGES = [(None, None), (None, 6), (6, None), (4, 10)] + def assert_method(self, py_value, method, *args, **kwargs): + ns_value = ns_from_py(py_value) + + try: + py_method = getattr(py_value, method) + except AttributeError: + self.fail("Python type '{}' does not have method '{}'".format(type(py_value), method)) + + try: + ns_method = getattr(ns_value, method) + except AttributeError: + self.fail("Rubicon analog for type '{}' does not have method '{}'".format(type(py_value), method)) + + try: + py_result = py_method(*args, **kwargs) + py_exception = None + except Exception as e: + py_exception = e + py_result = None + + try: + ns_result = ns_method(*args, **kwargs) + ns_exception = None + except Exception as e: + ns_exception = e + ns_result = None + + if py_exception is None and ns_exception is None: + self.assertEqual( + py_result, ns_result, + "Different results for {}: Python = {}; ObjC = {}".format(method, py_result, ns_result) + ) + elif py_exception is not None: + if ns_exception is not None: + self.assertEqual( + type(py_exception), type(ns_exception), + "Different exceptions for {}: Python = {}; ObjC = {}".format(method, py_result, ns_result) + ) + else: + self.fail("Python call for {} raised {}, but ObjC did not".format(method, type(py_exception))) + else: + self.fail("ObjC call for {} raised {}, but Python did not".format(method, type(py_exception))) + def test_str_nsstring_conversion(self): """Python str and NSString can be converted to each other manually.""" @@ -208,215 +251,173 @@ def test_nsstring_mul_rmul(self): self.assertEqual(n * ns_str, ns_repeated) def test_nsstring_capitalize(self): - ns_str = ns_from_py('lower, UPPER & Mixed!') - self.assertEqual(ns_str.capitalize(), 'Lower, upper & mixed!') + self.assert_method('lower, UPPER & Mixed!', 'capitalize') def test_nsstring_casefold(self): - ns_str = ns_from_py('lower, UPPER & Mixed!') - self.assertEqual(ns_str.casefold(), 'lower, upper & mixed!') + self.assert_method('lower, UPPER & Mixed!', 'casefold') def test_nsstring_center(self): - ns_str = ns_from_py('hello') - self.assertEqual(ns_str.center(20), ' hello ') - self.assertEqual(ns_str.center(20, '*'), '*******hello********') + self.assert_method('hello', 'center', 20) + self.assert_method('hello', 'center', 20, '*') def test_nsstring_count(self): - ns_str = ns_from_py('hello world') - self.assertEqual(ns_str.count('x'), 0) - self.assertEqual(ns_str.count('l'), 3) - self.assertEqual(ns_str.count('l', start=5), 1) - self.assertEqual(ns_str.count('l', end=8), 2) - self.assertEqual(ns_str.count('l', start=4, end=8), 0) + self.assert_method('hello world', 'count', 'x') + self.assert_method('hello world', 'count', 'l') def test_nsstring_encode(self): - ns_str = ns_from_py('Uñîçö∂€ string') - self.assertEqual( - ns_str.encode('utf-8'), - b'U\xc3\xb1\xc3\xae\xc3\xa7\xc3\xb6\xe2\x88\x82\xe2\x82\xac string' - ) - self.assertEqual( - ns_str.encode('utf-16'), - b'\xff\xfeU\x00\xf1\x00\xee\x00\xe7\x00\xf6\x00\x02"\xac \x00s\x00t\x00r\x00i\x00n\x00g\x00' - ) - - with self.assertRaises(UnicodeEncodeError): - ns_str.encode('ascii') - self.assertEqual(ns_str.encode('ascii', 'ignore'), b'U string') + self.assert_method('Uñîçö∂€ string', 'encode', 'utf-8') + self.assert_method('Uñîçö∂€ string', 'encode', 'utf-16') + self.assert_method('Uñîçö∂€ string', 'encode', 'ascii') + self.assert_method('Uñîçö∂€ string', 'encode', 'ascii', 'ignore') def test_nsstring_endswith(self): - ns_str = ns_from_py('Hello world') - self.assertTrue(ns_str.endswith('world')) - self.assertFalse(ns_str.endswith('cake')) + self.assert_method('world', 'endswith') + self.assert_method('cake', 'endswith') def test_nsstring_expandtabs(self): - ns_str = ns_from_py('hello\tworld') - self.assertEqual(ns_str.expandtabs(), 'hello world') - self.assertEqual(ns_str.expandtabs(4), 'hello world') - self.assertEqual(ns_str.expandtabs(10), 'hello world') + self.assert_method('hello\tworld', 'expandtabs', ) + self.assert_method('hello\tworld', 'expandtabs', 4) + self.assert_method('hello\tworld', 'expandtabs', 10) def test_nsstring_format(self): - ns_str = ns_from_py('hello {}') - self.assertEqual(ns_str.format('world'), 'hello world') + self.assert_method('hello {}', 'format', 'world') def test_nsstring_format_map(self): - ns_str = ns_from_py('hello {name}') - self.assertEqual(ns_str.format_map({'name': 'world'}), 'hello world') + self.assert_method('hello {name}', 'format_map', {'name': 'world'}) def test_nsstring_isalnum(self): - self.assertTrue(ns_from_py('abcd').isalnum()) - self.assertTrue(ns_from_py('1234').isalnum()) - self.assertTrue(ns_from_py('abcd1234').isalnum()) + self.assert_method('abcd', 'isalnum') + self.assert_method('1234', 'isalnum') + self.assert_method('abcd1234', 'isalnum') def test_nsstring_isalpha(self): - self.assertTrue(ns_from_py('abcd').isalpha()) - self.assertFalse(ns_from_py('1234').isalpha()) - self.assertFalse(ns_from_py('abcd1234').isalpha()) + self.assert_method('abcd', 'isalpha') + self.assert_method('1234', 'isalpha') + self.assert_method('abcd1234', 'isalpha') def test_nsstring_isdecimal(self): - self.assertFalse(ns_from_py('abcd').isdecimal()) - self.assertTrue(ns_from_py('1234').isdecimal()) - self.assertFalse(ns_from_py('abcd1234').isdecimal()) + self.assert_method('abcd', 'isdecimal') + self.assert_method('1234', 'isdecimal') + self.assert_method('abcd1234', 'isdecimal') def test_nsstring_isdigit(self): - self.assertFalse(ns_from_py('abcd').isdigit()) - self.assertTrue(ns_from_py('1234').isdigit()) - self.assertFalse(ns_from_py('abcd1234').isdigit()) + self.assert_method('abcd', 'isdigit') + self.assert_method('1234', 'isdigit') + self.assert_method('abcd1234', 'isdigit') def test_nsstring_isidentifier(self): - self.assertTrue(ns_from_py('def').isidentifier()) - self.assertTrue(ns_from_py('class').isidentifier()) - self.assertTrue(ns_from_py('hello').isidentifier()) - self.assertFalse(ns_from_py('boo!').isidentifier()) + self.assert_method('def', 'isidentifier') + self.assert_method('class', 'isidentifier') + self.assert_method('hello', 'isidentifier') + self.assert_method('boo!', 'isidentifier') def test_nsstring_islower(self): - self.assertTrue(ns_from_py('abcd').islower()) - self.assertFalse(ns_from_py('ABCD').islower()) - self.assertFalse(ns_from_py('1234').islower()) - self.assertTrue(ns_from_py('abcd1234').islower()) - self.assertFalse(ns_from_py('ABCD1234').islower()) + self.assert_method('abcd', 'islower') + self.assert_method('ABCD', 'islower') + self.assert_method('1234', 'islower') + self.assert_method('abcd1234', 'islower') + self.assert_method('ABCD1234', 'islower') def test_nsstring_isnumeric(self): - self.assertFalse(ns_from_py('abcd').isdigit()) - self.assertTrue(ns_from_py('1234').isdigit()) - self.assertFalse(ns_from_py('abcd1234').isdigit()) + self.assert_method('abcd', 'isdigit') + self.assert_method('1234', 'isdigit') + self.assert_method('abcd1234', 'isdigit') def test_nsstring_isprintable(self): - self.assertFalse(ns_from_py('\x09').isprintable()) - self.assertTrue(ns_from_py('Hello').isprintable()) + self.assert_method('\x09', 'isprintable') + self.assert_method('Hello', 'isprintable') def test_nsstring_isspace(self): - self.assertTrue(ns_from_py(' ').isspace()) - self.assertTrue(ns_from_py(' ').isspace()) - self.assertFalse(ns_from_py('Hello world').isspace()) - self.assertFalse(ns_from_py('Hello').isspace()) + self.assert_method(' ', 'isspace') + self.assert_method(' ', 'isspace') + self.assert_method('Hello world', 'isspace') + self.assert_method('Hello', 'isspace') def test_nsstring_istitle(self): - self.assertTrue(ns_from_py('Hello World').istitle()) - self.assertFalse(ns_from_py('hello world').istitle()) - self.assertFalse(ns_from_py('Hello world').istitle()) - self.assertFalse(ns_from_py('Hello WORLD').istitle()) - self.assertFalse(ns_from_py('HELLO WORLD').istitle()) + self.assert_method('Hello World', 'istitle') + self.assert_method('hello world', 'istitle') + self.assert_method('Hello world', 'istitle') + self.assert_method('Hello WORLD', 'istitle') + self.assert_method('HELLO WORLD', 'istitle') def test_nsstring_isupper(self): - self.assertFalse(ns_from_py('abcd').isupper()) - self.assertTrue(ns_from_py('ABCD').isupper()) - self.assertFalse(ns_from_py('1234').isupper()) - self.assertFalse(ns_from_py('abcd1234').isupper()) - self.assertTrue(ns_from_py('ABCD1234').isupper()) + self.assert_method('abcd', 'isupper') + self.assert_method('ABCD', 'isupper') + self.assert_method('1234', 'isupper') + self.assert_method('abcd1234', 'isupper') + self.assert_method('ABCD1234', 'isupper') def test_nsstring_join(self): - ns_str = ns_from_py(':') - self.assertEqual(ns_str.join(['aa', 'bb', 'cc']), 'aa:bb:cc') + self.assert_method(':', 'join', ['aa', 'bb', 'cc']) def test_nsstring_ljust(self): - ns_str = ns_from_py('123') - self.assertEqual(ns_str.ljust(5), '123 ') - self.assertEqual(ns_str.ljust(5, '*'), '123**') + self.assert_method('123', 'ljust', 5) + self.assert_method('123', 'ljust', 5, '*') def test_nsstring_lower(self): - ns_str = ns_from_py('lower, UPPER & Mixed!') - self.assertEqual(ns_str.lower(), 'lower, upper & mixed!') + self.assert_method('lower, UPPER & Mixed!', 'lower', ) def test_nsstring_lstrip(self): - ns_str = ns_from_py(' hello ') - self.assertEqual(ns_str.lstrip(), 'hello ') - - ns_str = ns_from_py('...hello...') - self.assertEqual(ns_str.lstrip('.'), 'hello...') + self.assert_method(' hello ', 'lstrip', ) + self.assert_method('...hello...', 'lstrip', ) + self.assert_method('...hello...', 'lstrip', '.') def test_nsstring_maketrans(self): - ns_str = ns_from_py('hello') - self.assertEqual(ns_str.maketrans('lo', 'g!'), {108: 103, 111: 33}) + self.assert_method('hello', 'maketrans', 'lo', 'g!') def test_nsstring_partition(self): - ns_str = ns_from_py('hello new world') - self.assertEqual(ns_str.partition(' '), ('hello', ' ', 'new world')) - self.assertEqual(ns_str.partition('l'), ('he', 'l', 'lo new world')) + self.assert_method('hello new world', 'partition', ' ') + self.assert_method('hello new world', 'partition', 'l') def test_nsstring_replace(self): - ns_str = ns_from_py('hello new world') - self.assertEqual(ns_str.replace('new', 'old'), 'hello old world') - self.assertEqual(ns_str.replace('l', '!'), 'he!!o new wor!d') - self.assertEqual(ns_str.replace('l', '!', 2), 'he!!o new world') + self.assert_method('hello new world', 'replace', 'new', 'old') + self.assert_method('hello new world', 'replace', 'l', '!') + self.assert_method('hello new world', 'replace', 'l', '!', 2) def test_nsstring_rjust(self): - ns_str = ns_from_py('123') - self.assertEqual(ns_str.rjust(5), ' 123') - self.assertEqual(ns_str.rjust(5, '*'), '**123') + self.assert_method('123', 'rjust', 5) + self.assert_method('123', 'rjust', 5, '*') def test_nsstring_rpartition(self): - ns_str = ns_from_py('hello new world') - self.assertEqual(ns_str.rpartition(' '), ('hello new', ' ', 'world')) - self.assertEqual(ns_str.rpartition('l'), ('hello new wor', 'l', 'd')) + self.assert_method('hello new world', 'rpartition', ' ') + self.assert_method('hello new world', 'rpartition', 'l') def test_nsstring_rsplit(self): - ns_str = ns_from_py('hello new world') - self.assertEqual(ns_str.rsplit(), ['hello', 'new', 'world']) - self.assertEqual(ns_str.rsplit(' ', 1), ['hello new', 'world']) - self.assertEqual(ns_str.rsplit('l'), ['he', '', 'o new wor', 'd']) - self.assertEqual(ns_str.rsplit('l', 2), ['hel', 'o new wor', 'd']) + self.assert_method('hello new world', 'rsplit') + self.assert_method('hello new world', 'rsplit', ' ', 1) + self.assert_method('hello new world', 'rsplit', 'l') + self.assert_method('hello new world', 'rsplit', 'l', 2) def test_nsstring_rstrip(self): - ns_str = ns_from_py(' hello ') - self.assertEqual(ns_str.rstrip(), ' hello') - - ns_str = ns_from_py('...hello...') - self.assertEqual(ns_str.rstrip('.'), '...hello') + self.assert_method(' hello ', 'rstrip') + self.assert_method('...hello...', 'rstrip') + self.assert_method('...hello...', 'rstrip', '.') def test_nsstring_split(self): - ns_str = ns_from_py('hello new world') - self.assertEqual(ns_str.split(), ['hello', 'new', 'world']) - self.assertEqual(ns_str.split(' ', 1), ['hello', 'new world']) - self.assertEqual(ns_str.split('l'), ['he', '', 'o new wor', 'd']) - self.assertEqual(ns_str.split('l', 2), ['he', '', 'o new world']) + self.assert_method('hello new world', 'split') + self.assert_method('hello new world', 'split', ' ', 1) + self.assert_method('hello new world', 'split', 'l') + self.assert_method('hello new world', 'split', 'l', 2) def test_nsstring_splitlines(self): - ns_str = ns_from_py('Hello\nnew\nworld\n') - self.assertEqual(ns_str.splitlines(), ['Hello', 'new', 'world']) + self.assert_method('Hello\nnew\nworld\n', 'splitlines') def test_nsstring_strip(self): - ns_str = ns_from_py(' hello ') - self.assertEqual(ns_str.strip(), 'hello') - - ns_str = ns_from_py('...hello...') - self.assertEqual(ns_str.strip('.'), 'hello') + self.assert_method(' hello ', 'strip') + self.assert_method('...hello...', 'strip') + self.assert_method('...hello...', 'strip', '.') def test_nsstring_swapcase(self): - ns_str = ns_from_py('lower, UPPER & Mixed!') - self.assertEqual(ns_str.swapcase(), 'LOWER, upper & mIXED!') + self.assert_method('lower, UPPER & Mixed!', 'swapcase') def test_nsstring_title(self): - ns_str = ns_from_py('lower, UPPER & Mixed!') - self.assertEqual(ns_str.title(), 'Lower, Upper & Mixed!') + self.assert_method('lower, UPPER & Mixed!', 'title') def test_nsstring_translate(self): - ns_str = ns_from_py('hello') - self.assertEqual(ns_str.translate({108: 'g', 111: '!!'}), 'hegg!!') + self.assert_method('hello', 'translate', {108: 'g', 111: '!!'}) def test_nsstring_upper(self): - ns_str = ns_from_py('lower, UPPER & Mixed!') - self.assertEqual(ns_str.upper(), 'LOWER, UPPER & MIXED!') + self.assert_method('lower, UPPER & Mixed!', 'upper') def test_nsstring_zfill(self): - ns_str = ns_from_py('123') - self.assertEqual(ns_str.zfill(5), '00123') + self.assert_method('123', 'zfill', 5) From 5b4d24f088e2c98f78585bfcd124f953427f050a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 30 May 2018 10:50:04 +0800 Subject: [PATCH 18/30] Clarified that string utility methods may not always return Python str. --- docs/how-to/type-mapping.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/how-to/type-mapping.rst b/docs/how-to/type-mapping.rst index b1606716..8975559c 100644 --- a/docs/how-to/type-mapping.rst +++ b/docs/how-to/type-mapping.rst @@ -76,13 +76,16 @@ keys). :class:`ObjCStrInstance` also handles Unicode code points above :class:`NSString` is based on UTF-16. If you have an :class:`ObjCStrInstance` instance, and you need to pass that -instance to a method that does a specific typecheck for `str`, you can use -:class:`str(nsstring)` to convert the :class:`ObjCStrInstance` instance to +instance to a method that does a specific typecheck for :class:`str`, you can +use ``str(nsstring)`` to convert the :class:`ObjCStrInstance` instance to :class:`str`. :class:`ObjCStrInstance` implements all the utility methods that are available -on :class:`str`, such as ``replace`` and ``split``. These utility methods all -return *Python* strings. +on :class:`str`, such as ``replace`` and ``split``. When these methods return +a string, the implementation may return Python :class:`str` or +:class:`ObjCStrInstance` instances. If you need to use the return value from +these methods, you should always use ``str()`` to ensure you have a Python +string. Lists ----- From 1d7a946d8f78cff8c0db7fdee2e8bd4ee559c35c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 30 May 2018 11:04:42 +0800 Subject: [PATCH 19/30] Replace explicit method definitions with getattr wrapper. --- rubicon/objc/collections.py | 124 +++--------------------------------- 1 file changed, 8 insertions(+), 116 deletions(-) diff --git a/rubicon/objc/collections.py b/rubicon/objc/collections.py index b9a6d3ec..30a4a814 100644 --- a/rubicon/objc/collections.py +++ b/rubicon/objc/collections.py @@ -178,134 +178,26 @@ def _index(self, sub, start=None, end=None, *, reverse): else: return found - def capitalize(self): - return str(self).capitalize() - - def casefold(self): - return str(self).casefold() - - def center(self, width, fillchar=' '): - return str(self).center(width, fillchar) - - def count(self, sub, start=None, end=None): - return str(self).count(sub, start, end) - - def encode(self, encoding='utf-8', errors='strict'): - return str(self).encode(encoding, errors=errors) - - def endswith(self, sub, start=None, end=None): - return str(self).endswith(sub, start, end) - - def expandtabs(self, tabsize=8): - return str(self).expandtabs(tabsize) - def find(self, sub, start=None, end=None): return self._find(sub, start=start, end=end, reverse=False) - def format(self, *args, **kwargs): - return str(self).format(*args, **kwargs) - - def format_map(self, mapping): - return str(self).format_map(mapping) - def index(self, sub, start=None, end=None): return self._index(sub, start=start, end=end, reverse=False) - def isalnum(self): - return str(self).isalnum() - - def isalpha(self): - return str(self).isalpha() - - def isdecimal(self): - return str(self).isdecimal() - - def isdigit(self): - return str(self).isdigit() - - def isidentifier(self): - return str(self).isidentifier() - - def islower(self): - return str(self).islower() - - def isnumeric(self): - return str(self).isnumeric() - - def isprintable(self): - return str(self).isprintable() - - def isspace(self): - return str(self).isspace() - - def istitle(self): - return str(self).istitle() - - def isupper(self): - return str(self).isupper() - - def join(self, iterable): - return str(self).join(iterable) - - def ljust(self, width, fillchar=' '): - return str(self).ljust(width, fillchar) - - def lower(self): - return str(self).lower() - - def lstrip(self, chars=None): - return str(self).lstrip(chars) - - def maketrans(self, x, *args, **kwargs): - return str(self).maketrans(x, *args, **kwargs) - - def partition(self, sep): - return str(self).partition(sep) - - def replace(self, old, new, count=-1): - return str(self).replace(old, new, count) - 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) - def rjust(self, width, fillchar=' '): - return str(self).rjust(width, fillchar) - - def rpartition(self, sep): - return str(self).rpartition(sep) - - def rsplit(self, sep=None, maxsplit=-1): - return str(self).rsplit(sep=sep, maxsplit=maxsplit) - - def rstrip(self, chars=None): - return str(self).rstrip(chars) - - def split(self, sep=None, maxsplit=-1): - return str(self).split(sep=sep, maxsplit=maxsplit) - - def splitlines(self, keepends=False): - return str(self).splitlines() - - def strip(self, chars=None): - return str(self).strip(chars) - - def swapcase(self): - return str(self).swapcase() - - def title(self): - return str(self).title() - - def translate(self, table): - return str(self).translate(table) - - def upper(self): - return str(self).upper() - - def zfill(self, width): - return str(self).zfill(width) + # 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) From f4e209f6dd0a3deb0dfa2a1bf29a7f488f7dd022 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Wed, 30 May 2018 14:00:27 +0200 Subject: [PATCH 20/30] Remove some rarely used .runtime imports in __init__ --- rubicon/objc/__init__.py | 4 +--- tests/test_core.py | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/rubicon/objc/__init__.py b/rubicon/objc/__init__.py index 317749a0..24f28339 100644 --- a/rubicon/objc/__init__.py +++ b/rubicon/objc/__init__.py @@ -17,9 +17,7 @@ NSEdgeInsetsMake, NSInteger, NSMakePoint, NSMakeRect, NSMakeSize, NSPoint, NSRange, NSRect, NSSize, NSTimeInterval, NSUInteger, NSZeroPoint, UIEdgeInsets, UIEdgeInsetsMake, UIEdgeInsetsZero, UniChar, unichar, ) -from .runtime import ( # noqa: F401 - IMP, SEL, Class, Ivar, Method, get_ivar, objc_id, objc_property_t, send_message, send_super, set_ivar, -) +from .runtime import SEL, send_message, send_super # noqa: F401 from .api import ( # noqa: F401 Block, NSArray, NSDictionary, NSMutableArray, NSMutableDictionary, NSObject, NSObjectProtocol, ObjCBlock, ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, ns_from_py, objc_classmethod, objc_const, objc_ivar, diff --git a/tests/test_core.py b/tests/test_core.py index 8a666892..e8568675 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -10,13 +10,12 @@ from enum import Enum from rubicon.objc import ( - SEL, NSEdgeInsets, NSEdgeInsetsMake, NSMakeRect, NSObject, - NSObjectProtocol, NSRange, NSRect, NSSize, NSUInteger, ObjCClass, - ObjCInstance, ObjCMetaClass, ObjCProtocol, at, get_ivar, objc_classmethod, - objc_const, objc_id, objc_ivar, objc_method, objc_property, send_message, send_super, set_ivar, types, + SEL, NSEdgeInsets, NSEdgeInsetsMake, NSMakeRect, NSObject, NSObjectProtocol, NSRange, NSRect, NSSize, NSUInteger, + ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, objc_classmethod, objc_const, objc_ivar, objc_method, + objc_property, send_message, send_super, types, ) from rubicon.objc.api import ObjCBoundMethod -from rubicon.objc.runtime import libobjc +from rubicon.objc.runtime import get_ivar, libobjc, objc_id, set_ivar try: import platform From c04cd8116d7795f08e7d0da5b503f01369638d62 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Wed, 30 May 2018 14:02:15 +0200 Subject: [PATCH 21/30] Remove dependency on exact type of methods in test_core --- tests/test_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index e8568675..9097e4fc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,7 +14,6 @@ ObjCClass, ObjCInstance, ObjCMetaClass, ObjCProtocol, at, objc_classmethod, objc_const, objc_ivar, objc_method, objc_property, send_message, send_super, types, ) -from rubicon.objc.api import ObjCBoundMethod from rubicon.objc.runtime import get_ivar, libobjc, objc_id, set_ivar try: @@ -439,7 +438,7 @@ def test_property_forcing(self): # Previously, it was a method. NSBundle = ObjCClass('NSBundle') NSBundle.declare_class_property('mainBundle') - self.assertFalse(type(NSBundle.mainBundle) == ObjCBoundMethod, 'NSBundle.mainBundle should not be a method') + self.assertFalse(callable(NSBundle.mainBundle), 'NSBundle.mainBundle should not be a method') def test_non_existent_field(self): "An attribute error is raised if you invoke a non-existent field." From b7c132d338943c826a00c7941a4c009e73547f57 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Wed, 30 May 2018 14:14:15 +0200 Subject: [PATCH 22/30] Make the load_or_error function from .runtime public On systems like iOS, this function is the only way to reliably load a library, --- rubicon/objc/runtime.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index 960c3764..46726665 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -24,6 +24,7 @@ 'get_ivar', 'libc', 'libobjc', + 'load_or_error', 'objc_block', 'objc_id', 'objc_method_description', @@ -50,7 +51,16 @@ _framework_path = ["/System/Library/Frameworks"] -def _load_or_error(name): +def load_or_error(name): + """Load and return the C library with the given name. + + If the library could not be found, a :class:`ValueError` is raised. + + Internally, this function uses :func:`ctypes.util.find_library` to search for the library in the system-standard + locations. If the library cannot be found this way, it is attempted to load the library from certain hardcoded + locations, as a fallback for systems where ``find_library`` does not work (such as iOS). + """ + path = util.find_library(name) if path is not None: return CDLL(path) @@ -74,9 +84,9 @@ def _load_or_error(name): raise ValueError("Library {!r} not found".format(name)) -libc = _load_or_error('c') -libobjc = _load_or_error('objc') -Foundation = _load_or_error('Foundation') +libc = load_or_error('c') +libobjc = load_or_error('objc') +Foundation = load_or_error('Foundation') @with_encoding(b'@') From 036a1b345614234e57f44cdd269f0c02ac8635a2 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Wed, 30 May 2018 14:22:20 +0200 Subject: [PATCH 23/30] Move common definitions in unit tests to the parent module --- tests/__init__.py | 18 ++++++++++++++++++ tests/test_NSArray.py | 17 ----------------- tests/test_NSDictionary.py | 17 ----------------- tests/test_NSString.py | 17 ----------------- tests/test_blocks.py | 18 +----------------- tests/test_core.py | 21 ++------------------- 6 files changed, 21 insertions(+), 87 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..977f17c6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,18 @@ +import faulthandler + +from ctypes import CDLL, util + +try: + import platform + OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split('.')[:2]) +except Exception: + OSX_VERSION = None + + +# Load the test harness library +rubiconharness_name = util.find_library('rubiconharness') +if rubiconharness_name is None: + raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") +rubiconharness = CDLL(rubiconharness_name) + +faulthandler.enable() diff --git a/tests/test_NSArray.py b/tests/test_NSArray.py index e5e10a32..4cfafcc4 100644 --- a/tests/test_NSArray.py +++ b/tests/test_NSArray.py @@ -1,27 +1,10 @@ -import faulthandler import unittest -from ctypes import CDLL, util from rubicon.objc import ( NSArray, NSMutableArray, NSObject, ObjCClass, objc_method, objc_property, ) from rubicon.objc.collections import ObjCListInstance -try: - import platform - OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split('.')[:2]) -except Exception: - OSX_VERSION = None - - -# Load the test harness library -rubiconharness_name = util.find_library('rubiconharness') -if rubiconharness_name is None: - raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") -rubiconharness = CDLL(rubiconharness_name) - -faulthandler.enable() - class NSArrayMixinTest(unittest.TestCase): py_list = ['one', 'two', 'three'] diff --git a/tests/test_NSDictionary.py b/tests/test_NSDictionary.py index f57e3d59..bb5ae454 100644 --- a/tests/test_NSDictionary.py +++ b/tests/test_NSDictionary.py @@ -1,6 +1,4 @@ -import faulthandler import unittest -from ctypes import CDLL, util from rubicon.objc import ( NSDictionary, NSMutableDictionary, NSObject, ObjCClass, objc_method, @@ -8,21 +6,6 @@ ) from rubicon.objc.collections import ObjCDictInstance -try: - import platform - OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split('.')[:2]) -except Exception: - OSX_VERSION = None - - -# Load the test harness library -rubiconharness_name = util.find_library('rubiconharness') -if rubiconharness_name is None: - raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") -rubiconharness = CDLL(rubiconharness_name) - -faulthandler.enable() - class NSDictionaryMixinTest(unittest.TestCase): py_dict = { diff --git a/tests/test_NSString.py b/tests/test_NSString.py index b5585a51..1faae2aa 100644 --- a/tests/test_NSString.py +++ b/tests/test_NSString.py @@ -1,25 +1,8 @@ -import faulthandler import unittest -from ctypes import CDLL, util from rubicon.objc import ns_from_py, py_from_ns from rubicon.objc.api import NSString -try: - import platform - OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split('.')[:2]) -except Exception: - OSX_VERSION = None - - -# Load the test harness library -rubiconharness_name = util.find_library('rubiconharness') -if rubiconharness_name is None: - raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") -rubiconharness = CDLL(rubiconharness_name) - -faulthandler.enable() - class NSStringTests(unittest.TestCase): TEST_STRINGS = ('', 'abcdef', 'Uñîçö∂€') diff --git a/tests/test_blocks.py b/tests/test_blocks.py index 06c0fde0..85cdb60f 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -1,26 +1,10 @@ -import faulthandler import unittest -from ctypes import CDLL, Structure, c_int, c_void_p, util +from ctypes import Structure, c_int, c_void_p from rubicon.objc import NSObject, ObjCBlock, ObjCClass, objc_method from rubicon.objc.api import Block from rubicon.objc.runtime import objc_block -try: - import platform - OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split('.')[:2]) -except Exception: - OSX_VERSION = None - - -# Load the test harness library -rubiconharness_name = util.find_library('rubiconharness') -if rubiconharness_name is None: - raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") -rubiconharness = CDLL(rubiconharness_name) - -faulthandler.enable() - class BlockTests(unittest.TestCase): def test_block_property_ctypes(self): diff --git a/tests/test_core.py b/tests/test_core.py index 9097e4fc..5e28c9b9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,11 +1,7 @@ -import faulthandler import functools import math import unittest -from ctypes import ( - CDLL, Structure, byref, c_char, c_double, c_float, c_int, c_void_p, cast, - create_string_buffer, util, -) +from ctypes import Structure, byref, c_char, c_double, c_float, c_int, c_void_p, cast, create_string_buffer from decimal import Decimal from enum import Enum @@ -16,20 +12,7 @@ ) from rubicon.objc.runtime import get_ivar, libobjc, objc_id, set_ivar -try: - import platform - OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split('.')[:2]) -except Exception: - OSX_VERSION = None - - -# Load the test harness library -rubiconharness_name = util.find_library('rubiconharness') -if rubiconharness_name is None: - raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") -rubiconharness = CDLL(rubiconharness_name) - -faulthandler.enable() +from . import OSX_VERSION, rubiconharness class struct_int_sized(Structure): From 1b7bd5f7d0b75039e3784242934e568d77d81e15 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Wed, 30 May 2018 14:23:38 +0200 Subject: [PATCH 24/30] Rename load_or_error to load_library --- rubicon/objc/runtime.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rubicon/objc/runtime.py b/rubicon/objc/runtime.py index 46726665..ccc27875 100644 --- a/rubicon/objc/runtime.py +++ b/rubicon/objc/runtime.py @@ -24,7 +24,7 @@ 'get_ivar', 'libc', 'libobjc', - 'load_or_error', + 'load_library', 'objc_block', 'objc_id', 'objc_method_description', @@ -51,7 +51,7 @@ _framework_path = ["/System/Library/Frameworks"] -def load_or_error(name): +def load_library(name): """Load and return the C library with the given name. If the library could not be found, a :class:`ValueError` is raised. @@ -84,9 +84,9 @@ def load_or_error(name): raise ValueError("Library {!r} not found".format(name)) -libc = load_or_error('c') -libobjc = load_or_error('objc') -Foundation = load_or_error('Foundation') +libc = load_library('c') +libobjc = load_library('objc') +Foundation = load_library('Foundation') @with_encoding(b'@') From b736bae0114d1cd3760d950a32d7300994da86d5 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Wed, 30 May 2018 14:27:44 +0200 Subject: [PATCH 25/30] Use load_library to load the unit test harness --- tests/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 977f17c6..0354f677 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,6 @@ import faulthandler -from ctypes import CDLL, util +from rubicon.objc.runtime import load_library try: import platform @@ -8,11 +8,9 @@ except Exception: OSX_VERSION = None - -# Load the test harness library -rubiconharness_name = util.find_library('rubiconharness') -if rubiconharness_name is None: - raise RuntimeError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") -rubiconharness = CDLL(rubiconharness_name) +try: + rubiconharness = load_library('rubiconharness') +except ValueError: + raise ValueError("Couldn't load Rubicon test harness library. Have you set DYLD_LIBRARY_PATH?") faulthandler.enable() From 573808781bc994d55da3b8d496dd5538ffeaf30e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 May 2018 08:29:30 +0800 Subject: [PATCH 26/30] Added examples of string type conversions. Also added a note on the usage of at(). --- docs/how-to/type-mapping.rst | 40 +++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/how-to/type-mapping.rst b/docs/how-to/type-mapping.rst index 8975559c..fed75b28 100644 --- a/docs/how-to/type-mapping.rst +++ b/docs/how-to/type-mapping.rst @@ -65,7 +65,16 @@ If a method returns an :class:`NSString`, the return value will be a wrapped :class:`ObjCStrInstance` type. This type implements a :class:`str`-like interface, wrapped around the underlying :class:`NSString` data. This means you can treat the return value as if it were a string - slicing it, -concatenating it with other strings, comparing it, and so on. +concatenating it with other strings, comparing it, and so on:: + + # Call an Objective C method that returns a string. + # We're using NSBundle to give us a string version of a path + >>> NSBundle.mainBundle.bundlePath + + + # Slice the Objective C string + >>> NSBundle.mainBundle.bundlePath[:14] + Note that :class:`ObjCStrInstance` objects behave slightly differently than Python :class:`str` objects in some cases. For technical reasons, @@ -78,14 +87,39 @@ keys). :class:`ObjCStrInstance` also handles Unicode code points above If you have an :class:`ObjCStrInstance` instance, and you need to pass that instance to a method that does a specific typecheck for :class:`str`, you can use ``str(nsstring)`` to convert the :class:`ObjCStrInstance` instance to -:class:`str`. +:class:`str`:: + + # Convert the Objective C string to a Python string. + >>> str(NSBundle.mainBundle.bundlePath) + '/Users/rkm/projects/beeware/venv3.6/bin' + +Conversely, if you have a :class:`str`, and you specifically require a +:class:`ObjCStrInstance` instance, you can use the :meth:`at()` method to +convert the Python instance to an :class:`ObjCStrInstance`. + + >>> from rubicon.objc import at + # Create a Python string + >>> py_str = 'hello world' + + # Convert to an Objective C string + >>> at(py_str) + :class:`ObjCStrInstance` implements all the utility methods that are available on :class:`str`, such as ``replace`` and ``split``. When these methods return a string, the implementation may return Python :class:`str` or :class:`ObjCStrInstance` instances. If you need to use the return value from these methods, you should always use ``str()`` to ensure you have a Python -string. +string:: + + # Is the path comprised of all lowercase letters? (Hint: it isn't) + >>> NSBundle.mainBundle.bundlePath.islower() + False + + # Convert string to lower case; use str() to ensure we get a Python string. + >>> str(NSBundle.mainBundle.bundlePath.lower()) + '/users/rkm/projects/beeware/venv3.6/bin' + Lists ----- From 14230af36e89af87b57fae825f1567f46074696f Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 3 Jun 2018 21:29:57 -0400 Subject: [PATCH 27/30] Enforces Python version 3.5 and above Signed-off-by: Dan Yeaw --- setup.py | 4 +++- tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 57614058..e86e7e56 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ author_email='russell@keith-magee.com', url='http://pybee.org/rubicon', packages=find_packages(exclude=['tests']), + python_requires='>=3.5', namespace_packages=['rubicon'], license='New BSD', classifiers=[ @@ -33,8 +34,9 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Objective C', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development', ], diff --git a/tox.ini b/tox.ini index ddd71894..9ad9b655 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,13 @@ # and then run "tox" from this directory. [tox] -envlist = {py34,py35,py36,pypy3}-{default,i386},flake8 +envlist = {py35,py36,py37,pypy3}-{default,i386},flake8 [testenv] basepython = - py34: python3.4 py35: python3.5 py36: python3.6 + py37: python3.7 pypy3: pypy3 whitelist_externals = From f13b3c09fb8b934bc2adf213b2e1069b0921e99c Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Sun, 10 Jun 2018 21:20:40 -0400 Subject: [PATCH 28/30] Change required python version to 3.4 or above, remove version 3.7 Signed-off-by: Dan Yeaw --- setup.py | 4 ++-- tox.ini | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e86e7e56..70a0cf2d 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ author_email='russell@keith-magee.com', url='http://pybee.org/rubicon', packages=find_packages(exclude=['tests']), - python_requires='>=3.5', + python_requires='>=3.4', namespace_packages=['rubicon'], license='New BSD', classifiers=[ @@ -34,9 +34,9 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Objective C', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development', ], diff --git a/tox.ini b/tox.ini index 9ad9b655..ddd71894 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,13 @@ # and then run "tox" from this directory. [tox] -envlist = {py35,py36,py37,pypy3}-{default,i386},flake8 +envlist = {py34,py35,py36,pypy3}-{default,i386},flake8 [testenv] basepython = + py34: python3.4 py35: python3.5 py36: python3.6 - py37: python3.7 pypy3: pypy3 whitelist_externals = From 4eb5ea20724cefd043391bfa0d85b1020e38b617 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 13 Jun 2018 07:01:16 +0800 Subject: [PATCH 29/30] Added a success stories page. --- docs/background/index.rst | 1 + docs/background/success.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 docs/background/success.rst diff --git a/docs/background/index.rst b/docs/background/index.rst index be8cc8a5..534e6f04 100644 --- a/docs/background/index.rst +++ b/docs/background/index.rst @@ -12,5 +12,6 @@ plans for the future? That's what you'll find here! faq community + success releases roadmap diff --git a/docs/background/success.rst b/docs/background/success.rst new file mode 100644 index 00000000..e65d530f --- /dev/null +++ b/docs/background/success.rst @@ -0,0 +1,6 @@ +Success Stories +=============== + +Want to see examples of Rubicon in use? Here's some: + +* `Travel Tips `_ is an app in the iOS App Store that uses Rubicon to access the iOS UIKit libraries. From 6ac7ea5d5846c548690362b9e317ab291febc59c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 13 Jun 2018 07:10:54 +0800 Subject: [PATCH 30/30] Markup correction. --- docs/background/releases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/background/releases.rst b/docs/background/releases.rst index c1cdcf80..4fe66e77 100644 --- a/docs/background/releases.rst +++ b/docs/background/releases.rst @@ -9,7 +9,7 @@ Release History * Removed automatic conversion of ``NSString`` objects to ``str`` when returned from Objective-C methods. This feature made it difficult to call Objective-C methods on ``NSString`` objects, because there was no easy way to prevent the automatic conversion. * In most cases, this change will not affect existing code, because ``NSString`` objects now support operations similar to ``str``. * If an actual ``str`` object is required, the ``NSString`` object can be wrapped in a ``str`` call to convert it. -* Added support for ``objc_property``s with non-object types. +* Added support for ``objc_property`` with non-object types. * Added public ``get_ivar`` and ``set_ivar`` functions for manipulating ivars. * Changed the implementation of ``objc_property`` to use ivars instead of Python attributes for storage. This fixes name conflicts in some situations. * Fixed ``objc_property`` setters on non-macOS platforms. (cculianu)