Skip to content

Commit

Permalink
Improve partial method system beeware#148 beeware#453
Browse files Browse the repository at this point in the history
  • Loading branch information
qqfunc committed Apr 30, 2024
1 parent 2585345 commit 88a0d89
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 41 deletions.
70 changes: 29 additions & 41 deletions src/rubicon/objc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,28 +246,30 @@ def __repr__(self):
return f"{type(self).__qualname__}({self.name_start!r})"

def __call__(self, receiver, first_arg=_sentinel, **kwargs):
if first_arg is ObjCPartialMethod._sentinel:
# Ignore parts of argument names after "__".
order = [argname.split("__")[0] for argname in kwargs]
args = [arg for arg in kwargs.values()]

if first_arg is self._sentinel:
if kwargs:
# This will be deleted when #26 is introduced.
raise TypeError("Missing first (positional) argument")

args = []
rest = frozenset()
rest = (*order,)
else:
args = [first_arg]
# Add "" to rest to indicate that the method takes arguments
rest = frozenset(kwargs) | frozenset(("",))
args.insert(0, first_arg)
rest = ("", *order)

try:
name, order = self.methods[rest]
name = self.methods[rest]
except KeyError:
raise ValueError(
f"No method was found starting with {self.name_start!r} and with keywords {set(kwargs)}\n"
f"No method was found starting with {self.name_start!r} and with keywords {(*kwargs,)}\n"
f"Known keywords are:\n"
+ "\n".join(repr(keywords) for keywords in self.methods)
)

meth = receiver.objc_class._cache_method(name)
args += [kwargs[name] for name in order]

return meth(receiver, *args)


Expand Down Expand Up @@ -1035,28 +1037,11 @@ def __getattr__(self, name):
The "interleaved" syntax is usually preferred, since it looks more
similar to normal Objective-C syntax. However, the "flat" syntax is also
fully supported. Certain method names require the "flat" syntax, for
example if two arguments have the same label (e.g.
``performSelector:withObject:withObject:``), which is not supported by
Python's keyword argument syntax.
.. warning::
The "interleaved" syntax currently ignores the ordering of its
keyword arguments. However, in the interest of readability, the
keyword arguments should always be passed in the same order as they
appear in the method name.
This also means that two methods whose names which differ only in
the ordering of their keywords will conflict with each other, and
can only be called reliably using "flat" syntax.
As of Python 3.6, the order of keyword arguments passed to functions
is preserved (:pep:`468`). In the future, once Rubicon requires
Python 3.6 or newer, "interleaved" method calls will respect keyword
argument order. This will fix the kind of conflict described above,
but will also disallow specifying the keyword arguments out of
order.
fully supported. If two arguments have the same label (e.g.
``performSelector:withObject:withObject:``), you can use ``__`` in the
keywords like ``performSelector(..., withObject__1=...,
withObject__2=...)``. The parts after ``__`` can be anything you can use
as argument names.
"""
# Search for named instance method in the class object and if it
# exists, return callable object with self as hidden argument.
Expand Down Expand Up @@ -1090,7 +1075,7 @@ def __getattr__(self, name):
else:
method = None

if method is None or set(method.methods) == {frozenset()}:
if method is None or set(method.methods) == {()}:
# Find a method whose full name matches the given name if no partial
# method was found, or the partial method can only resolve to a
# single method that takes no arguments. The latter case avoids
Expand Down Expand Up @@ -1654,20 +1639,23 @@ def _load_methods(self):
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] == ""
# Selectors end with a colon if the method takes arguments.
if name.endswith(":"):
first, *rest, _ = name.split(":")
# Insert an empty string in order to indicate that the method
# takes a first argument as a positional argument.
rest.insert(0, "")
rest = tuple(rest)
else:
first = name
rest = ()

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)] = (name, order)
partial.methods[rest] = name

# Set the list of methods for the class to the computed list.
self.methods_ptr = methods_ptr
Expand Down
4 changes: 4 additions & 0 deletions tests/objc/Example.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ extern NSString *const SomeGlobalStringConstant;
+(NSUInteger) overloaded;
+(NSUInteger) overloaded:(NSUInteger)arg1;
+(NSUInteger) overloaded:(NSUInteger)arg1 extraArg:(NSUInteger)arg2;
+(NSUInteger) overloaded:(NSUInteger)arg extraArg1:(NSUInteger)arg1 extraArg2:(NSUInteger)arg2;
+(NSUInteger) overloaded:(NSUInteger)arg extraArg2:(NSUInteger)arg2 extraArg1:(NSUInteger)arg1;
+(NSUInteger) overloaded:(NSUInteger)arg orderedArg1:(NSUInteger)arg1 orderedArg2:(NSUInteger)arg2;
+(NSUInteger) overloaded:(NSUInteger)arg duplicateArg:(NSUInteger)arg1 duplicateArg:(NSUInteger)arg2;

+(struct complex) doStuffWithStruct:(struct simple)simple;

Expand Down
20 changes: 20 additions & 0 deletions tests/objc/Example.m
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,26 @@ +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg:(NSUInteger)arg2
return arg1 + arg2;
}

+(NSUInteger) overloaded:(NSUInteger)arg extraArg1:(NSUInteger)arg1 extraArg2:(NSUInteger)arg2
{
return 1;
}

+(NSUInteger) overloaded:(NSUInteger)arg extraArg2:(NSUInteger)arg2 extraArg1:(NSUInteger)arg1
{
return 2;
}

+(NSUInteger) overloaded:(NSUInteger)arg orderedArg1:(NSUInteger)arg1 orderedArg2:(NSUInteger)arg2
{
return 0;
}

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

+(struct complex) doStuffWithStruct:(struct simple)simple
{
return (struct complex){
Expand Down
16 changes: 16 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,22 @@ def test_partial_method_lots_of_args(self):
)
self.assertEqual(buf.value.decode("utf-8"), pystring)

def test_partial_method_arg_order(self):
Example = ObjCClass("Example")

self.assertEqual(Example.overloaded(0, extraArg1=0, extraArg2=0), 1)
self.assertEqual(Example.overloaded(0, extraArg2=0, extraArg1=0), 2)

with self.assertRaises(ValueError):
Example.overloaded(0, orderedArg2=0, orderedArg1=0)

def test_partial_method_duplicate_arg_names(self):
Example = ObjCClass("Example")
self.assertEqual(
Example.overloaded(0, duplicateArg__a=14, duplicateArg__b=24),
14 + 24,
)

def test_objcmethod_str_repr(self):
"""Test ObjCMethod, ObjCPartialMethod, and ObjCBoundMethod str and repr"""

Expand Down

0 comments on commit 88a0d89

Please sign in to comment.