Repository: jpype-project/jpype (language: python)

Issue:
Ambiguous Overloads for functional interfaces with different argument counts
# Description

When a Java method have multiple overloads with functional interfaces, an Ambiguous Overloads Type Error is raised when attempting to call the method with a Python Function.

In particular, this is valid in Java:

```java
public class MyClass {
    public static String apply(Function<String, String> mapper) {
        return mapper.apply("1");
    }

    public static String apply(BiFunction<String, String> mapper) {
        return mapper.apply("1", "2");
    }

    public static void main(String[] args) {
        System.out.println(MyClass.apply(number -> "Number " + number))
        // "Number 1"
        System.out.println(MyClass.apply((number1, number2) -> "Number " + number1 + " and " + number2))
        // "Number 1 and 2"
    }
}
```

But this is not valid in Python
```python
import jpype
import jpype.imports

jpype.startJVM(classpath=['...'])


def single_arg_function(arg):
    return f'Number {arg}'


def two_arg_function(arg1, arg2):
    return f'Number {arg1} and {arg2}'


from org.acme import MyClass
assert str(MyClass.apply(single_arg_function)) == 'Number 1'
assert str(MyClass.apply(two_arg_function)) == 'Number 1 and 2'
```

```
TypeError: Ambiguous overloads found for org.acme.MyClass.apply(function) between:
public static java.lang.String org.acme.MyClass.apply(java.util.function.BiFunction)
public static java.lang.String org.acme.MyClass.apply(java.util.function.Function)
```

# Workaround

If you know exactly the functional interfaces you are using, cast to proxies using `inspect.signature`

```python
def cast(python_function):
    parameters = inspect.signature(python_function).parameters
    arg_count = len(parameters)
    if arg_count == 1:
        return JObject(python_function, Function)
    elif arg_count == 2:
        return JObject(python_function, BiFunction)
    else:
        raise Exception(f'Unsupported number of arguments: {arg_count}')


assert str(MyClass.apply(cast(single_arg_function))) == 'Number 1'
assert str(MyClass.apply(cast(two_arg_function))) == 'Number 1 and 2'
```

# Use Case

Make it easier to use Java Libraries in Python. Getting an ambiguous type error confuse Python users unfamiliar with Java, potentially causing them to abandon trying to use the technology. There are a few places in OptaPlanner (particular `Joiners`) where there are multiple overloads, same number of args, but different Functional Interfaces (with different parameter counts). Thus, it possible to do something like `Joiners.lessThan(item -> item.id, otherItem -> otherItem.item.id)` and `Joiners.lessThan((item1, item2) -> item2.id, otherItem -> otherItem.item.id)`  because the functional interfaces have a different argument count. I would like Python users to have the same experience:  `Joiners.lessThan(lambda item: item.id, lambda otherItem: otherItem.item.id)` and `Joiners.lessThan(lambda item1, item2: item2.id, lambda otherItem: otherItem.item.id)`. Currently, to achieve this behaviour, I would need to create a custom Joiners class that uses the casting workaround posted above.

# Reproducer
https://github.com/Christopher-Chianelli/issue-reproducer/tree/jpype-ambiguous-types-for-functional-interfaces-with-different-argument-counts

# Expected Behaviour

If there are overloads with the same argument counts but have functional interfaces with different argument counts, and a function is passed, inspect the function parameter count and cast it to the functional interface that have the same number of arguments. If no interface have the same number of parameters, raise a No Overload found Error. This does not solve the case where the Functional interfaces have the same parameter count, where casting must be used (the same as Java).


Discussion / hints:
Why not just use the casting operator?   You should just be able to cast to Function or Bifunction to choose an overload using @.  That is what is tested in the test cases.
Simple: I don't want to force my users to use casting operators. I am trying to hide the fact Java is even used. (A side note, the casting operator via `@` is not documented in https://jpype.readthedocs.io/en/latest ; it recommends using `JObject(python_object, type)`. Hitting this "Ambiguous overloads found" will surprise the user, and discourage them from continuing. I am fine working around it (as I work with Java and know what types to use) but plain Python users, who know nothing about Java (or the types) do not.
(FWIW, how to extract python function argument count in C: https://stackoverflow.com/a/1117735 )
I did an experiment with passing functions with a different number of arguments than the interface to Java:

```python
import jpype
import jpype.imports
from jpype import JImplements, JOverride, JObject

jpype.startJVM(classpath=['target/issue-reproducer-8.11.0.Final.jar'])

from java.util.function import Function, BiFunction

def single_arg_function(arg):
    return f'Number {arg}'


def two_arg_function(arg1, arg2):
    return f'Number {arg1} and {arg2}'


from org.acme import MyClass

# Accept Function
print(str(MyClass.noOverloadApply(two_arg_function)))

# Accept BiFunction
print(str(MyClass.noOverloadApply2(single_arg_function)))
```

Result: Python raises a TypeError for both:
```
print(str(MyClass.noOverloadApply2(single_arg_function)))
TypeError: single_arg_function() takes 1 positional argument but 2 were given
```

```
print(str(MyClass.noOverloadApply(two_arg_function)))
TypeError: two_arg_function() missing 1 required positional argument: 'arg2'
```

Which means making `single_arg_function` not being castable to BiFunction will not break backward compatibility (unless someone is checking the message).

I'll try an attempt at this.
There is one extra case that need to be considered: default arguments (i.e. the following):

```
def two_arg_function(arg1, arg2=None):
    return f'Number {arg1} and {arg2}'
 
```

in which case `two_arg_function` is both a Function and a BiFunction

Fix patch:
diff --git a/native/common/include/jp_typemanager.h b/native/common/include/jp_typemanager.h
index 5cec58d4c..6038d86ee 100644
--- a/native/common/include/jp_typemanager.h
+++ b/native/common/include/jp_typemanager.h
@@ -41,6 +41,7 @@ class JPTypeManager
 	JPClass* findClassForObject(jobject obj);
 	void populateMethod(void* method, jobject obj);
 	void populateMembers(JPClass* cls);
+    int interfaceParameterCount(JPClass* cls);
 
 private:
 	JPContext* m_Context;
@@ -50,6 +51,7 @@ class JPTypeManager
 	jmethodID m_FindClassForObject;
 	jmethodID m_PopulateMethod;
 	jmethodID m_PopulateMembers;
+    jmethodID m_InterfaceParameterCount;
 } ;
 
 #endif // _JPCLASS_H_
\ No newline at end of file
diff --git a/native/common/jp_functional.cpp b/native/common/jp_functional.cpp
index f94dba927..eaf0282ef 100644
--- a/native/common/jp_functional.cpp
+++ b/native/common/jp_functional.cpp
@@ -40,9 +40,65 @@ class JPConversionFunctional : public JPConversion
 	{
 		if (!PyCallable_Check(match.object))
 			return match.type = JPMatch::_none;
-		match.conversion = this;
-		match.closure = cls;
-		return match.type = JPMatch::_implicit;
+
+		// Modified from https://stackoverflow.com/a/1117735
+		bool has_parameter_count = false;
+		// Get the __code__ attribute of the function, which contains details about the function
+		// Note: __code__ is func_code in Python 2.x
+		PyObject* function_code_object = PyObject_GetAttrString(match.object, "__code__");
+		if(function_code_object) {
+			// get the argument count
+			PyObject* parameter_count_obj = PyObject_GetAttrString(function_code_object, "co_argcount");
+			if(parameter_count_obj) {
+				has_parameter_count = true;
+				int optional_parameter_count = 0;
+				int flags = 0;
+				const int ACCEPT_VARGS_MASK = 0x04; // From https://docs.python.org/3/reference/datamodel.html
+				PyObject* flags_obj = PyObject_GetAttrString(function_code_object, "co_flags");
+				if (flags_obj) {
+					flags = PyLong_AsLong(flags_obj);
+					Py_DECREF(flags_obj);
+				}
+				PyObject* optional_parameter_tuple = PyObject_GetAttrString(match.object, "__defaults__");
+				if (optional_parameter_tuple && optional_parameter_tuple != Py_None) {
+					optional_parameter_count = PyTuple_Size(optional_parameter_tuple);
+					Py_DECREF(optional_parameter_tuple);
+				}
+				const int parameter_count = PyLong_AsLong(parameter_count_obj);
+				const int java_parameter_count = cls->getContext()->getTypeManager()->interfaceParameterCount(cls);
+				const bool is_varargs = (flags & ACCEPT_VARGS_MASK) == ACCEPT_VARGS_MASK;
+
+				// def my_func(x, y=None) should be both a Function and a BiFunction
+				// i.e. the number of parameters accepted by the interface MUST
+				// 1. Be at most the maximum number of parameters accepted by the python function (parameter_count)
+				//    (Unless the function accept a variable number of arguments, then this restriction does not
+				//     apply).
+				// 2. Be at least the minumum number of parameters accepted by the python function
+				// (parameter_count - optional_parameter_count = number of required parameters).
+				// Notes:
+				// - keywords vargs does not remove restriction 1
+				// - keyword only arguments are not counted.
+				if ((!is_varargs && parameter_count < java_parameter_count) ||
+					parameter_count - optional_parameter_count > java_parameter_count) {
+					match.type = JPMatch::_none;
+				} else {
+					match.conversion = this;
+					match.closure = cls;
+					match.type = JPMatch::_implicit;
+				}
+
+				Py_DECREF(parameter_count_obj);
+			}
+			Py_DECREF(function_code_object);
+		}
+		PyErr_Clear();
+		if (!has_parameter_count) {
+			match.conversion = this;
+			match.closure = cls;
+			return match.type = JPMatch::_implicit;
+		} else {
+			return match.type;
+		}
 	}
 
 	virtual void getInfo(JPClass *cls, JPConversionInfo &info) override
diff --git a/native/common/jp_methoddispatch.cpp b/native/common/jp_methoddispatch.cpp
index 3778dff4a..d1c5af5e8 100644
--- a/native/common/jp_methoddispatch.cpp
+++ b/native/common/jp_methoddispatch.cpp
@@ -61,6 +61,9 @@ bool JPMethodDispatch::findOverload(JPJavaFrame& frame, JPMethodMatch &bestMatch
 		// Anything better than explicit constitutes a hit on the cache
 		if (bestMatch.m_Type > JPMatch::_explicit)
 			return true;
+		else
+			// bad match so forget the overload.
+			bestMatch.m_Overload = 0;
 	}
 
 	// We need two copies of the match.  One to hold the best match we have
diff --git a/native/common/jp_typemanager.cpp b/native/common/jp_typemanager.cpp
index 91edaeb06..82ff13f60 100644
--- a/native/common/jp_typemanager.cpp
+++ b/native/common/jp_typemanager.cpp
@@ -27,6 +27,7 @@ JPTypeManager::JPTypeManager(JPJavaFrame& frame)
 	m_FindClassForObject = frame.GetMethodID(cls, "findClassForObject", "(Ljava/lang/Object;)J");
 	m_PopulateMethod = frame.GetMethodID(cls, "populateMethod", "(JLjava/lang/reflect/Executable;)V");
 	m_PopulateMembers = frame.GetMethodID(cls, "populateMembers", "(Ljava/lang/Class;)V");
+    m_InterfaceParameterCount = frame.GetMethodID(cls, "interfaceParameterCount", "(Ljava/lang/Class;)I");
 
 	// The object instance will be loaded later
 	JP_TRACE_OUT;
@@ -97,3 +98,13 @@ void JPTypeManager::populateMembers(JPClass* cls)
 	frame.CallVoidMethodA(m_JavaTypeManager.get(), m_PopulateMembers, val);
 	JP_TRACE_OUT;
 }
+
+int JPTypeManager::interfaceParameterCount(JPClass *cls)
+{
+	JP_TRACE_IN("JPTypeManager::interfaceParameterCount");
+	JPJavaFrame frame = JPJavaFrame::outer(m_Context);
+	jvalue val[1];
+	val[0].l = (jobject) cls->getJavaClass();
+	return frame.CallIntMethodA(m_JavaTypeManager.get(), m_InterfaceParameterCount, val);
+	JP_TRACE_OUT;
+}
diff --git a/native/java/org/jpype/manager/ClassDescriptor.java b/native/java/org/jpype/manager/ClassDescriptor.java
index 265853586..aefe1aff8 100644
--- a/native/java/org/jpype/manager/ClassDescriptor.java
+++ b/native/java/org/jpype/manager/ClassDescriptor.java
@@ -46,6 +46,7 @@ public class ClassDescriptor
   public int methodCounter = 0;
   public long[] fields;
   public long anonymous;
+  public int functional_interface_parameter_count = -1; // -1 = unset; -2 = not a functional interface
 
   ClassDescriptor(Class cls, long classPtr)
   {
diff --git a/native/java/org/jpype/manager/TypeManager.java b/native/java/org/jpype/manager/TypeManager.java
index a36f8d7de..3cdd9ea9d 100644
--- a/native/java/org/jpype/manager/TypeManager.java
+++ b/native/java/org/jpype/manager/TypeManager.java
@@ -300,6 +300,34 @@ public synchronized void populateMethod(long wrapper, Executable method)
     }
   }
 
+  /**
+   * Returns the number of arguments an interface only unimplemented method accept.
+   *
+   * @param interfaceClass The class of the interface
+   * @return the number of arguments the only unimplemented method of the interface accept.
+   */
+  public int interfaceParameterCount(Class<?> interfaceClass) {
+    ClassDescriptor classDescriptor = classMap.get(interfaceClass);
+    if (classDescriptor.functional_interface_parameter_count != -1) {
+      return classDescriptor.functional_interface_parameter_count;
+    }
+    int abstractMethodParameterCount = -1;
+    for (Method method : interfaceClass.getMethods()) {
+      if (Modifier.isAbstract(method.getModifiers())) {
+        if (abstractMethodParameterCount != -1) {
+          classDescriptor.functional_interface_parameter_count = -2;
+          return -2;
+        }
+        abstractMethodParameterCount = method.getParameterCount();
+      }
+    }
+    if (abstractMethodParameterCount == -1) {
+      abstractMethodParameterCount = -2;
+    }
+    classDescriptor.functional_interface_parameter_count = abstractMethodParameterCount;
+    return abstractMethodParameterCount;
+  }
+
   /**
    * Get a class for an object.
    *


Test patch:
diff --git a/test/harness/jpype/overloads/Test2.java b/test/harness/jpype/overloads/Test2.java
new file mode 100644
index 000000000..304e34418
--- /dev/null
+++ b/test/harness/jpype/overloads/Test2.java
@@ -0,0 +1,48 @@
+/* ****************************************************************************
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.overloads;
+
+import java.util.List;
+
+public class Test2
+{
+    @FunctionalInterface
+    public interface NoArg {
+        public String apply();
+    }
+
+    @FunctionalInterface
+    public interface SingleArg {
+        public String apply(int a);
+    }
+
+    @FunctionalInterface
+    public interface TwoArg {
+        public String apply(int a, int b);
+    }
+
+    public String testFunctionalInterfaces(NoArg noArg) {
+        return "NoArgs";
+    }
+
+    public String testFunctionalInterfaces(SingleArg singleArg) {
+        return "SingleArg";
+    }
+
+    public String testFunctionalInterfaces(TwoArg twoArg) {
+        return "TwoArg";
+    }
+}
\ No newline at end of file
diff --git a/test/jpypetest/test_overloads.py b/test/jpypetest/test_overloads.py
index d33467493..1e54e1f3f 100644
--- a/test/jpypetest/test_overloads.py
+++ b/test/jpypetest/test_overloads.py
@@ -190,3 +190,36 @@ def testDefaultMethods(self):
             pass
         else:
             self.assertEqual('B', testdefault.defaultMethod())
+
+    def testFunctionalInterfacesWithDifferentSignatures(self):
+        test2 = self.__jp.Test2()
+        self.assertEqual('NoArgs', test2.testFunctionalInterfaces(lambda: 'NoArgs'))
+        self.assertEqual('SingleArg', test2.testFunctionalInterfaces(lambda a: 'SingleArg'))
+        self.assertEqual('TwoArg', test2.testFunctionalInterfaces(lambda a, b: 'TwoArg'))
+
+    def testFunctionalInterfacesWithDefaults(self):
+        def my_fun(x, y=None):
+            return 'my_fun'
+
+        def my_fun_vargs(x, *vargs):
+            return 'my_fun_vargs'
+
+        def my_fun_kwargs(x, **kwargs):
+            return 'my_fun_kwargs'
+
+        def my_fun_kw(*, keyword_arg=None):
+            return 'my_fun_kw'
+
+        class my_fun_class:
+            def __call__(self):
+                return 'my_fun_class'
+
+        test2 = self.__jp.Test2()
+        self.assertRaisesRegex(
+            TypeError, 'Ambiguous overloads found', test2.testFunctionalInterfaces, my_fun)
+        self.assertRaisesRegex(
+            TypeError, 'Ambiguous overloads found', test2.testFunctionalInterfaces, my_fun_vargs)
+        self.assertRaisesRegex(
+            TypeError, 'Ambiguous overloads found', test2.testFunctionalInterfaces, my_fun_class)
+        self.assertEqual('SingleArg', test2.testFunctionalInterfaces(my_fun_kwargs))
+        self.assertEqual('NoArgs', test2.testFunctionalInterfaces(my_fun_kw))