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

Issue:
java.lang.Class JConversion is ignored when looking for overloads for a method
When a JConversion is registered for `java.lang.Class`, it is silently ignored

Example:
```java
package org.acme;

public class MyClass {
    public static String getClassSimpleName(Class<?> clazz) {
        return clazz.getSimpleName();
    }
}
```

```python
import jpype.imports
from jpype import _jcustomizer
from jpype import JClass

class ProxyClassInstance:
    def __init__(self, proxy):
        self.proxy = proxy


@_jcustomizer.JConversion("java.lang.Class", instanceof=ProxyClassInstance)
def _ProxyClassConvert(jcls, obj):
    return obj.proxy

pype.startJVM(classpath=['target/issue-reproducer-8.11.0.Final.jar'])

from org.acme import MyClass

out = MyClass.getClassSimpleName(JClass('java.lang.String'))
assert out == 'String'

out = MyClass.getClassSimpleName(ProxyClassInstance(JClass('java.lang.String')))
assert out == 'String'
```

Expected:
Terminated without output OR an Error is raised when the JConversion is defined saying "conversion to java.lang.Class is not allowed"

Actual:
```
TypeError: No matching overloads found for *static* org.acme.MyClass.getClassSimpleName(ProxyClassInstance), options are:
	public static java.lang.String org.acme.MyClass.getClassSimpleName(java.lang.Class)
```

JConversion does work for other types (like `java.util.Collection`)

Use Case:
OptaPlanner has a lot of interfaces that take classes in order to determine what Objects to send in constraint streams:
Ex:
```java
constraintFactory.from(Lesson.class)
    .join(Lesson.class, Joiners.equal(Lesson::getRoom))
    .penalize("Same Room", HardSoftScore.ONE_HARD)
```

OptaPy generates proxy Java classes for Python class, which then can be reference in the constraint stream. It is stored on the type itself, meaning it is possible to do an implicit conversion from type to Java class by referencing its java class attribute, allowing for the OptaPy code to look like this:

```python
constraint_factory.from_(Lesson) \
    .join(Lesson, Joiners.equal(lambda lesson: lesson.room) \
    .penalize("Same Room", HardSoftScore.ONE_HARD)
``` 

However, this issue is preventing such an implicit conversion being registered, forcing the python code to look like this instead:


```python
LessonClass = get_class(Lesson)

constraint_factory.from_(LessonClass) \
    .join(LessonClass, Joiners.equal(lambda lesson: lesson.room) \
    .penalize("Same Room", HardSoftScore.ONE_HARD)
``` 

Reproducer:
https://github.com/Christopher-Chianelli/issue-reproducer/tree/jpype-conversion-customization


Discussion / hints:
I don't really see anything wrong with your code so something is odd here.   Class is somewhat special in that is is loaded very early in the process but the classhints structure should allow retroactive conversions to be added.   I will have to get back to you likely next weekend as my workload is rather high.

Fix patch:
diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst
index b782d44ad..a2f78d348 100644
--- a/doc/CHANGELOG.rst
+++ b/doc/CHANGELOG.rst
@@ -6,8 +6,11 @@ This changelog *only* contains changes from the *first* pypi release (0.5.4.3) o
 Latest Changes:
 - **1.3.1_dev0 - 2021-06-05**
 
+  - Support for user defined conversions for java.lang.Class and array types.
+
   - Fixed issue with ssize_t on Windows for Python 3.10.
 
+
 - **1.3.0 - 2021-05-19**
 
   - Fixes for memory issues found when upgrading to Python 3.10 beta.
diff --git a/native/common/jp_arrayclass.cpp b/native/common/jp_arrayclass.cpp
index afc557e57..c719af0e6 100644
--- a/native/common/jp_arrayclass.cpp
+++ b/native/common/jp_arrayclass.cpp
@@ -43,6 +43,7 @@ JPMatch::Type JPArrayClass::findJavaConversion(JPMatch &match)
 			|| charArrayConversion->matches(this, match)
 			|| byteArrayConversion->matches(this, match)
 			|| sequenceConversion->matches(this, match)
+			|| hintsConversion->matches(this, match)
 			)
 		return match.type;
 	JP_TRACE("None");
@@ -57,6 +58,7 @@ void JPArrayClass::getConversionInfo(JPConversionInfo &info)
 	charArrayConversion->getInfo(this, info);
 	byteArrayConversion->getInfo(this, info);
 	sequenceConversion->getInfo(this, info);
+	hintsConversion->getInfo(this, info);
 	PyList_Append(info.ret, PyJPClass_create(frame, this).get());
 }
 
diff --git a/native/common/jp_classtype.cpp b/native/common/jp_classtype.cpp
index b79f573a7..f02c30e25 100644
--- a/native/common/jp_classtype.cpp
+++ b/native/common/jp_classtype.cpp
@@ -36,11 +36,10 @@ JPClassType::~JPClassType()
 JPMatch::Type JPClassType::findJavaConversion(JPMatch& match)
 {
 	JP_TRACE_IN("JPClass::findJavaConversion");
-	if (nullConversion->matches(this, match) != JPMatch::_none)
-		return match.type;
-	if (objectConversion->matches(this, match) != JPMatch::_none)
-		return match.type;
-	if (classConversion->matches(this, match) != JPMatch::_none)
+	if (nullConversion->matches(this, match)
+			|| objectConversion->matches(this, match)
+			|| classConversion->matches(this, match)
+			|| hintsConversion->matches(this, match))
 		return match.type;
 	return match.type = JPMatch::_none;
 	JP_TRACE_OUT;
@@ -52,5 +51,6 @@ void JPClassType::getConversionInfo(JPConversionInfo &info)
 	nullConversion->getInfo(this, info);
 	objectConversion->getInfo(this, info);
 	classConversion->getInfo(this, info);
+	hintsConversion->getInfo(this, info);
 	PyList_Append(info.ret, PyJPClass_create(frame, this).get());
-}
\ No newline at end of file
+}


Test patch:
diff --git a/test/jpypetest/test_classhints.py b/test/jpypetest/test_classhints.py
index 1c3c9b72b..bcf895622 100644
--- a/test/jpypetest/test_classhints.py
+++ b/test/jpypetest/test_classhints.py
@@ -24,6 +24,21 @@ def blah(self):
         pass
 
 
+class ClassProxy:
+    def __init__(self, proxy):
+        self.proxy = proxy
+
+
+class ArrayProxy:
+    def __init__(self, proxy):
+        self.proxy = proxy
+
+
+class StringProxy:
+    def __init__(self, proxy):
+        self.proxy = proxy
+
+
 class ClassHintsTestCase(common.JPypeTestCase):
 
     def setUp(self):
@@ -85,3 +100,30 @@ def StrToCustom(jcls, args):
         cht.call(MyImpl())
         self.assertIsInstance(cht.input, self.MyCustom)
         self.assertIsInstance(cht.input.arg, MyImpl)
+
+    def testClassCustomizer(self):
+
+        @jpype.JConversion("java.lang.Class", instanceof=ClassProxy)
+        def ClassCustomizer(jcls, obj):
+            return obj.proxy
+
+        hints = jpype.JClass('java.lang.Class')._hints
+        self.assertTrue(ClassProxy in hints.implicit)
+
+    def testArrayCustomizer(self):
+
+        @jpype.JConversion(jpype.JInt[:], instanceof=ArrayProxy)
+        def ArrayCustomizer(jcls, obj):
+            return obj.proxy
+
+        hints = jpype.JClass(jpype.JInt[:])._hints
+        self.assertTrue(ArrayProxy in hints.implicit)
+
+    def testStringCustomizer(self):
+
+        @jpype.JConversion("java.lang.String", instanceof=StringProxy)
+        def STringCustomizer(jcls, obj):
+            return obj.proxy
+
+        hints = jpype.JClass("java.lang.String")._hints
+        self.assertTrue(StringProxy in hints.implicit)