Skip to content

Commit

Permalink
Implement nicer method call syntax using keyword arguments (for beewa…
Browse files Browse the repository at this point in the history
  • Loading branch information
dgelessus committed Mar 12, 2017
1 parent 6d7d1ea commit 37238ff
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 10 deletions.
73 changes: 73 additions & 0 deletions rubicon/objc/objc.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,40 @@ def __call__(self, receiver, *args, convert_args=True, convert_result=True):
result = ObjCClass(result)
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 with selector parts {}".format(set(kwargs)))

meth = ObjCMethod(meth)
args += [kwargs[name] for name in order]
return meth(receiver, *args)

######################################################################

Expand Down Expand Up @@ -1229,6 +1263,28 @@ def __getattr__(self, 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:
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)
Expand Down Expand Up @@ -1342,6 +1398,8 @@ def __new__(cls, *args):
'instance_methods': {},
# Mapping of name -> (accessor method, mutator method)
'instance_properties': {},
# 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.
Expand Down Expand Up @@ -1383,11 +1441,26 @@ def _reload_methods(self):
self.methods_ptr = objc.class_copyMethodList(self, byref(self.methods_ptr_count))
# old_methods_ptr may be None, but free(NULL) is a no-op, so that's fine.
c.free(old_methods_ptr)

for i in range(self.methods_ptr_count.value):
method = self.methods_ptr[i]
name = objc.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:
partial = self.partial_methods[first] = ObjCPartialMethod(first)

# 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."""
Expand Down
4 changes: 4 additions & 0 deletions tests/objc/Example.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@ struct large {
-(NSString *) getMessage;
-(NSString *) reverseIt:(NSString *) input;

+(NSUInteger) overloaded;
+(NSUInteger) overloaded:(NSUInteger)arg1;
+(NSUInteger) overloaded:(NSUInteger)arg1 extraArg:(NSUInteger)arg2;

@end
15 changes: 15 additions & 0 deletions tests/objc/Example.m
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,19 @@ -(NSString *) reverseIt:(NSString *) input
return [self.callback reverse:input];
}

+(NSUInteger) overloaded
{
return 0;
}

+(NSUInteger) overloaded:(NSUInteger)arg1
{
return arg1;
}

+(NSUInteger) overloaded:(NSUInteger)arg1 extraArg:(NSUInteger)arg2
{
return arg1 + arg2;
}

@end
52 changes: 42 additions & 10 deletions tests/test_rubicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import faulthandler
faulthandler.enable()

from rubicon.objc import ObjCInstance, ObjCClass, ObjCMetaClass, NSObject, SEL, objc, objc_method, objc_classmethod, objc_property, NSEdgeInsets, NSEdgeInsetsMake, send_message
from rubicon.objc import ObjCInstance, ObjCClass, ObjCMetaClass, NSObject, SEL, objc, objc_method, objc_classmethod, objc_property, NSUInteger, NSRange, NSEdgeInsets, NSEdgeInsetsMake, send_message
from rubicon.objc import core_foundation


Expand Down Expand Up @@ -448,26 +448,28 @@ def test_struct_return(self):
Example = ObjCClass('Example')
example = Example.alloc().init()

# FIXME: Overriding the restype like done below is NOT reliable - this code may need to be updated if the method lookup internals change. When #14 is fixed, there should be a better way of setting a custom restype (or it should be set correctly by default).

class struct_int_sized(Structure):
_fields_ = [("x", c_char * 4)]

example.intSizedStruct
example.objc_class.instance_methods["intSizedStruct"].restype = struct_int_sized
self.assertEqual(example.intSizedStruct().x, b"abc")
method = example.intSizedStruct
method.method.restype = struct_int_sized
self.assertEqual(method().x, b"abc")

class struct_oddly_sized(Structure):
_fields_ = [("x", c_char * 5)]

example.oddlySizedStruct
example.objc_class.instance_methods["oddlySizedStruct"].restype = struct_oddly_sized
self.assertEqual(example.oddlySizedStruct().x, b"abcd")
method = example.oddlySizedStruct
method.method.restype = struct_oddly_sized
self.assertEqual(method().x, b"abcd")

class struct_large(Structure):
_fields_ = [("x", c_char * 17)]

example.largeStruct
example.objc_class.instance_methods["largeStruct"].restype = struct_large
self.assertEqual(example.largeStruct().x, b"abcdefghijklmnop")
method = example.largeStruct
method.method.restype = struct_large
self.assertEqual(method().x, b"abcdefghijklmnop")

def test_struct_return_send(self):
"Methods returning structs of different sizes by value can be handled when using send_message."
Expand Down Expand Up @@ -510,6 +512,36 @@ def test_no_convert_return(self):
res = example.toString(convert_result=False)
self.assertNotIsInstance(res, ObjCInstance)
self.assertEqual(str(ObjCInstance(res)), "This is an ObjC Example object")

def test_partial_method_no_args(self):
Example = ObjCClass("Example")
self.assertEqual(Example.overloaded(), 0)

def test_partial_method_one_arg(self):
Example = ObjCClass("Example")
self.assertEqual(Example.overloaded(42), 42)

def test_partial_method_two_args(self):
Example = ObjCClass("Example")
self.assertEqual(Example.overloaded(12, extraArg=34), 12+34)

def test_partial_method_lots_of_args(self):
pystring = "Uñîçö∂€"
pybytestring = pystring.encode("utf-8")
nsstring = core_foundation.at(pystring)
buf = create_string_buffer(len(pybytestring) + 1)
usedLength = NSUInteger()
remaining = NSRange(0, 0)
nsstring.getBytes(
buf,
maxLength=32,
usedLength=byref(usedLength),
encoding=4, # NSUTF8StringEncoding
options=0,
range=NSRange(0, 7),
remainingRange=byref(remaining),
)
self.assertEqual(buf.value.decode("utf-8"), pystring)

def test_duplicate_class_registration(self):
"If you define a class name twice in the same runtime, you get an error."
Expand Down

0 comments on commit 37238ff

Please sign in to comment.