diff --git a/src/rubicon/objc/api.py b/src/rubicon/objc/api.py index 6bb9be0b..63b25e53 100644 --- a/src/rubicon/objc/api.py +++ b/src/rubicon/objc/api.py @@ -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) @@ -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. @@ -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 @@ -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 diff --git a/tests/objc/Example.h b/tests/objc/Example.h index f6025524..7cf83d9e 100644 --- a/tests/objc/Example.h +++ b/tests/objc/Example.h @@ -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; diff --git a/tests/objc/Example.m b/tests/objc/Example.m index 746de128..df9b7600 100644 --- a/tests/objc/Example.m +++ b/tests/objc/Example.m @@ -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){ diff --git a/tests/test_core.py b/tests/test_core.py index 7f14f9ee..55fa22e1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -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"""