From 1331a91f6f3b93c71bdf6f275f7cfa32d21fff6f Mon Sep 17 00:00:00 2001 From: rbasralian Date: Wed, 24 Jan 2024 16:43:35 -0500 Subject: [PATCH] Function for converting Python values to an explicit Java type (#128) * Fix a bug in cast * Add jpy.convert method to convert a Python value to a Java object of the given type. This leverages the existing JPy_AsJObjectWithType method. * Add .jclassname to jpy type. * Update some tests to match current behavior (e.g. returning Byte instead of Integer) * More logging during tests * Fixed failing hashNegativeOne test * Replace incorrect return Py_NONEs with Py_RETURN_NONE (for correct reference counting) --- .gitignore | 1 + README.md | 6 + doc/reference.rst | 5 +- setup.py | 1 + src/main/c/jni/org_jpy_PyLib.c | 4 +- src/main/c/jpy_conv.c | 4 +- src/main/c/jpy_conv.h | 5 +- src/main/c/jpy_jmethod.c | 6 +- src/main/c/jpy_jobj.c | 4 +- src/main/c/jpy_jtype.c | 15 +- src/main/c/jpy_module.c | 109 ++++++- .../java/org/jpy/EmbeddableTestJunit.java | 6 + src/test/java/org/jpy/JavaReflectionTest.java | 5 + src/test/java/org/jpy/LifeCycleTest.java | 6 + src/test/java/org/jpy/PyLibTest.java | 4 + .../java/org/jpy/PyLibWithSysPathTest.java | 4 + src/test/java/org/jpy/PyModuleTest.java | 3 + src/test/java/org/jpy/PyObjectTest.java | 14 +- src/test/java/org/jpy/PyProxyTest.java | 9 +- src/test/java/org/jpy/TestStatePrinter.java | 31 ++ .../fixtures/TypeConversionTestFixture.java | 4 + src/test/python/jpy_exception_test.py | 22 +- src/test/python/jpy_overload_test.py | 5 +- src/test/python/jpy_translation_test.py | 2 +- src/test/python/jpy_typeconv_test.py | 276 +++++++++++++++++- src/test/python/jpy_typeconv_test_pyobj.py | 61 ++++ 26 files changed, 556 insertions(+), 56 deletions(-) create mode 100644 src/test/java/org/jpy/TestStatePrinter.java create mode 100644 src/test/python/jpy_typeconv_test_pyobj.py diff --git a/.gitignore b/.gitignore index b0b770c0..ce26acb3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ Vagrantfile *.so *.dll +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 116ad046..b21176e0 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,12 @@ documentation. 4. Update documentation, `cd doc` and run `make html` 5. http://peterdowns.com/posts/first-time-with-pypi.html + +Running Tests +---------------- + +Run: `python setup.py build test` + Automated builds ---------------- diff --git a/doc/reference.rst b/doc/reference.rst index c6d2fb6c..95f4b5cb 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -87,7 +87,10 @@ jpy Functions tm = rt.totalMemory() The returned Java types have a `jclass` attribute which returns the actual Java object. This allows for using - the Java types where a Java method would expect a parameter of type `java.lang.Class`. + the Java types where a Java method would expect a parameter of type `java.lang.Class`. They also have a `jclassname` + attribute, which returns the Java type associated with the 'obj' reference. The `jclassname` is the reference's + declared type (which may be a supertype), rather than the object's runtime type (as returned by + `obj.getClass().getName()`). To instantiate Java array objects, the :py:func:`jpy.array()` function is used. diff --git a/setup.py b/setup.py index 009bafdd..84a7a9ff 100644 --- a/setup.py +++ b/setup.py @@ -88,6 +88,7 @@ os.path.join(src_test_py_dir, 'jpy_exception_test.py'), os.path.join(src_test_py_dir, 'jpy_overload_test.py'), os.path.join(src_test_py_dir, 'jpy_typeconv_test.py'), + os.path.join(src_test_py_dir, 'jpy_typeconv_test_pyobj.py'), os.path.join(src_test_py_dir, 'jpy_typeres_test.py'), os.path.join(src_test_py_dir, 'jpy_modretparam_test.py'), os.path.join(src_test_py_dir, 'jpy_translation_test.py'), diff --git a/src/main/c/jni/org_jpy_PyLib.c b/src/main/c/jni/org_jpy_PyLib.c index 3073d02c..ef7ca1e7 100644 --- a/src/main/c/jni/org_jpy_PyLib.c +++ b/src/main/c/jni/org_jpy_PyLib.c @@ -2611,7 +2611,7 @@ static PyObject* JPrint_write(PyObject* self, PyObject* args) } fprintf(stdout, "%s", text); } - return Py_BuildValue(""); + Py_RETURN_NONE; } static PyObject* JPrint_flush(PyObject* self, PyObject* args) @@ -2619,7 +2619,7 @@ static PyObject* JPrint_flush(PyObject* self, PyObject* args) if (stdout != NULL) { fflush(stdout); } - return Py_BuildValue(""); + Py_RETURN_NONE; } static PyMethodDef JPrint_Functions[] = { diff --git a/src/main/c/jpy_conv.c b/src/main/c/jpy_conv.c index e7d64534..493dc0c8 100644 --- a/src/main/c/jpy_conv.c +++ b/src/main/c/jpy_conv.c @@ -205,7 +205,7 @@ PyObject* JPy_FromJString(JNIEnv* jenv, jstring stringRef) jint length; if (stringRef == NULL) { - return Py_BuildValue(""); + Py_RETURN_NONE; } length = (*jenv)->GetStringLength(jenv, stringRef); @@ -227,7 +227,7 @@ PyObject* JPy_FromJString(JNIEnv* jenv, jstring stringRef) const char* utfChars; if (stringRef == NULL) { - return Py_BuildValue(""); + Py_RETURN_NONE; } utfChars = (*jenv)->GetStringUTFChars(jenv, stringRef, NULL); diff --git a/src/main/c/jpy_conv.h b/src/main/c/jpy_conv.h index f1dfab64..f6efb28e 100644 --- a/src/main/c/jpy_conv.h +++ b/src/main/c/jpy_conv.h @@ -60,8 +60,9 @@ extern "C" { #endif -#define JPy_FROM_JVOID() Py_BuildValue("") -#define JPy_FROM_JNULL() Py_BuildValue("") +#define JPy_PY_NONE() Py_BuildValue("") // Builds a Python 'None' object +#define JPy_FROM_JVOID() JPy_PY_NONE() +#define JPy_FROM_JNULL() JPy_PY_NONE() /** diff --git a/src/main/c/jpy_jmethod.c b/src/main/c/jpy_jmethod.c index f77b5ebd..934c6c60 100644 --- a/src/main/c/jpy_jmethod.c +++ b/src/main/c/jpy_jmethod.c @@ -631,7 +631,7 @@ PyObject* JMethod_set_param_mutable(JPy_JMethod* self, PyObject* args) } JMethod_CHECK_PARAMETER_INDEX(self, index); self->paramDescriptors[index].isMutable = value; - return Py_BuildValue(""); + Py_RETURN_NONE; } PyObject* JMethod_is_param_output(JPy_JMethod* self, PyObject* args) @@ -661,7 +661,7 @@ PyObject* JMethod_set_param_output(JPy_JMethod* self, PyObject* args) } JMethod_CHECK_PARAMETER_INDEX(self, index); self->paramDescriptors[index].isOutput = value; - return Py_BuildValue(""); + Py_RETURN_NONE; } PyObject* JMethod_is_param_return(JPy_JMethod* self, PyObject* args) @@ -694,7 +694,7 @@ PyObject* JMethod_set_param_return(JPy_JMethod* self, PyObject* args) if (value) { self->returnDescriptor->paramIndex = index; } - return Py_BuildValue(""); + Py_RETURN_NONE; } diff --git a/src/main/c/jpy_jobj.c b/src/main/c/jpy_jobj.c index e7fcf66c..d8247dc1 100644 --- a/src/main/c/jpy_jobj.c +++ b/src/main/c/jpy_jobj.c @@ -79,7 +79,7 @@ PyObject* JObj_FromType(JNIEnv* jenv, JPy_JType* type, jobject objectRef) if (PyCallable_Check(callable)) { callableResult = PyObject_CallFunction(callable, "OO", type, obj); if (callableResult == NULL) { - return Py_None; + Py_RETURN_NONE; } else { return callableResult; } @@ -355,7 +355,7 @@ PyObject* JObj_str(JPy_JObj* self) JPy_GET_JNI_ENV_OR_RETURN(jenv, NULL) if (self->objectRef == NULL) { - return Py_BuildValue(""); + Py_RETURN_NONE; } returnValue = NULL; diff --git a/src/main/c/jpy_jtype.c b/src/main/c/jpy_jtype.c index 5fff4e11..fd7aee27 100644 --- a/src/main/c/jpy_jtype.c +++ b/src/main/c/jpy_jtype.c @@ -914,19 +914,19 @@ int JType_ConvertPythonToJavaObject(JNIEnv* jenv, JPy_JType* type, PyObject* pyA return JType_CreateJavaDoubleObject(jenv, type, pyArg, objectRef); } else if (type == JPy_JPyObject) { return JType_CreateJavaPyObject(jenv, type, pyArg, objectRef); - } else if (JPy_IS_STR(pyArg) && (type == JPy_JString || type == JPy_JObject || ((*jenv)->IsAssignableFrom(jenv, JPy_JString->classRef, type->classRef)))) { + } else if (JPy_IS_STR(pyArg) && (type == JPy_JString || type == JPy_JObject || (*jenv)->IsAssignableFrom(jenv, JPy_JString->classRef, type->classRef))) { return JPy_AsJString(jenv, pyArg, objectRef); - } else if (PyBool_Check(pyArg) && (type == JPy_JObject || ((*jenv)->IsAssignableFrom(jenv, JPy_Boolean_JClass, type->classRef)))) { + } else if (PyBool_Check(pyArg) && (type == JPy_JObject || (*jenv)->IsAssignableFrom(jenv, JPy_Boolean_JClass, type->classRef))) { return JType_CreateJavaBooleanObject(jenv, type, pyArg, objectRef); } else if (JPy_IS_CLONG(pyArg) && (type == JPy_JObject || (*jenv)->IsAssignableFrom(jenv, JPy_Number_JClass, type->classRef))) { return JType_CreateJavaNumberFromPythonInt(jenv, type, pyArg, objectRef); } else if (JPy_IS_CLONG(pyArg) && ((*jenv)->IsAssignableFrom(jenv, JPy_Integer_JClass, type->classRef))) { return JType_CreateJavaIntegerObject(jenv, type, pyArg, objectRef); - } else if (JPy_IS_CLONG(pyArg) && (type == JPy_JObject || ((*jenv)->IsAssignableFrom(jenv, JPy_Long_JClass, type->classRef)))) { + } else if (JPy_IS_CLONG(pyArg) && (*jenv)->IsAssignableFrom(jenv, JPy_Long_JClass, type->classRef)) { return JType_CreateJavaLongObject(jenv, type, pyArg, objectRef); - } else if (PyFloat_Check(pyArg) && (type == JPy_JObject || ((*jenv)->IsAssignableFrom(jenv, JPy_Double_JClass, type->classRef)))) { + } else if (PyFloat_Check(pyArg) && (type == JPy_JObject || (*jenv)->IsAssignableFrom(jenv, JPy_Double_JClass, type->classRef))) { return JType_CreateJavaDoubleObject(jenv, type, pyArg, objectRef); - } else if (PyFloat_Check(pyArg) && (type == JPy_JObject || ((*jenv)->IsAssignableFrom(jenv, JPy_Float_JClass, type->classRef)))) { + } else if (PyFloat_Check(pyArg) && (*jenv)->IsAssignableFrom(jenv, JPy_Float_JClass, type->classRef)) { return JType_CreateJavaFloatObject(jenv, type, pyArg, objectRef); } else if (type == JPy_JObject && allowObjectWrapping) { return JType_CreateJavaPyObject(jenv, JPy_JPyObject, pyArg, objectRef); @@ -1298,6 +1298,7 @@ int JType_AddClassAttribute(JNIEnv* jenv, JPy_JType* declaringClass) return -1; } PyDict_SetItem(typeDict, Py_BuildValue("s", "jclass"), (PyObject*) JObj_FromType(jenv, JPy_JClass, declaringClass->classRef)); + PyDict_SetItem(typeDict, Py_BuildValue("s", "jclassname"), (PyObject*) JPy_FROM_CSTR(declaringClass->typeObj.tp_name)); } return 0; } @@ -1446,10 +1447,10 @@ PyObject* JType_GetOverloadedMethod(JNIEnv* jenv, JPy_JType* type, PyObject* met } else if (type != JPy_JObject && JPy_JObject != NULL) { return JType_GetOverloadedMethod(jenv, JPy_JObject, methodName, JNI_FALSE); } else { - return Py_None; + Py_RETURN_NONE; } } else { - return Py_None; + Py_RETURN_NONE; } } diff --git a/src/main/c/jpy_module.c b/src/main/c/jpy_module.c index ccf8563f..f29cd040 100644 --- a/src/main/c/jpy_module.c +++ b/src/main/c/jpy_module.c @@ -38,6 +38,7 @@ PyObject* JPy_create_jvm(PyObject* self, PyObject* args, PyObject* kwds); PyObject* JPy_destroy_jvm(PyObject* self, PyObject* args); PyObject* JPy_get_type(PyObject* self, PyObject* args, PyObject* kwds); PyObject* JPy_cast(PyObject* self, PyObject* args); +PyObject* JPy_convert(PyObject* self, PyObject* args); PyObject* JPy_array(PyObject* self, PyObject* args); PyObject* JPy_byte_buffer(PyObject* self, PyObject* args); @@ -61,6 +62,11 @@ static PyMethodDef JPy_Functions[] = { "cast(obj, type) - Cast the given Java object to the given Java type (type name or type object). " "Returns None if the cast is not possible."}, + {"convert", JPy_convert, METH_VARARGS, + "convert(obj, type) - Convert the given Python object to the given Java type (type name or type object). " + "Returns None if the conversion is not possible. If the Java type is a primitive, the returned object " + "will be of the corresponding boxed type."}, + {"array", JPy_array, METH_VARARGS, "array(name, init) - Return a new Java array of given Java type (type name or type object) and initializer (array length or sequence). " "Possible primitive types are 'boolean', 'byte', 'char', 'short', 'int', 'long', 'float', and 'double'."}, @@ -451,7 +457,7 @@ PyObject* JPy_create_jvm(PyObject* self, PyObject* args, PyObject* kwds) if (JPy_JVM != NULL) { JPy_DIAG_PRINT(JPy_DIAG_F_JVM + JPy_DIAG_F_ERR, "JPy_create_jvm: WARNING: Java VM is already running.\n"); JPy_DECREF(options); - return Py_BuildValue(""); + Py_RETURN_NONE; } if (!PySequence_Check(options)) { @@ -513,7 +519,7 @@ PyObject* JPy_create_jvm(PyObject* self, PyObject* args, PyObject* kwds) return NULL; } - return Py_BuildValue(""); + Py_RETURN_NONE; } PyObject* JPy_destroy_jvm(PyObject* self, PyObject* args) @@ -526,7 +532,7 @@ PyObject* JPy_destroy_jvm(PyObject* self, PyObject* args) JPy_JVM = NULL; } - return Py_BuildValue(""); + Py_RETURN_NONE; } PyObject* JPy_get_type_internal(JNIEnv* jenv, PyObject* self, PyObject* args, PyObject* kwds) @@ -551,16 +557,16 @@ PyObject* JPy_get_type(PyObject* self, PyObject* args, PyObject* kwds) PyObject* JPy_cast_internal(JNIEnv* jenv, PyObject* self, PyObject* args) { PyObject* obj; - PyObject* objType; - JPy_JType* type; + PyObject* targetTypeArg; // can be a string PyObject (i.e. JPy_IS_STR) or a JPy_JType + JPy_JType* targetTypeParsed; // the actual type that targetTypeArg is processed as jboolean inst; - if (!PyArg_ParseTuple(args, "OO:cast", &obj, &objType)) { + if (!PyArg_ParseTuple(args, "OO:cast", &obj, &targetTypeArg)) { return NULL; } if (obj == Py_None) { - return Py_BuildValue(""); + Py_RETURN_NONE; } if (!JObj_Check(obj)) { @@ -568,24 +574,24 @@ PyObject* JPy_cast_internal(JNIEnv* jenv, PyObject* self, PyObject* args) return NULL; } - if (JPy_IS_STR(objType)) { - const char* typeName = JPy_AS_UTF8(objType); - type = JType_GetTypeForName(jenv, typeName, JNI_FALSE); - if (type == NULL) { + if (JPy_IS_STR(targetTypeArg)) { + const char* typeName = JPy_AS_UTF8(targetTypeArg); + targetTypeParsed = JType_GetTypeForName(jenv, typeName, JNI_FALSE); + if (targetTypeParsed == NULL) { return NULL; } - } else if (JType_Check(objType)) { - type = (JPy_JType*) objType; + } else if (JType_Check(targetTypeArg)) { + targetTypeParsed = (JPy_JType*) targetTypeArg; } else { PyErr_SetString(PyExc_ValueError, "cast: argument 2 (obj_type) must be a Java type name or Java type object"); return NULL; } - inst = (*jenv)->IsInstanceOf(jenv, ((JPy_JObj*) obj)->objectRef, type->classRef); + inst = (*jenv)->IsInstanceOf(jenv, ((JPy_JObj*) obj)->objectRef, targetTypeParsed->classRef); if (inst) { - return (PyObject*) JObj_FromType(jenv, (JPy_JType*) objType, ((JPy_JObj*) obj)->objectRef); + return (PyObject*) JObj_FromType(jenv, (JPy_JType*) targetTypeParsed, ((JPy_JObj*) obj)->objectRef); } else { - return Py_BuildValue(""); + Py_RETURN_NONE; } } @@ -594,6 +600,77 @@ PyObject* JPy_cast(PyObject* self, PyObject* args) JPy_FRAME(PyObject*, NULL, JPy_cast_internal(jenv, self, args), 16) } +PyObject* JPy_convert_internal(JNIEnv* jenv, PyObject* self, PyObject* args) +{ + PyObject* obj; + PyObject* targetTypeArg; // can be a string PyObject (i.e. JPy_IS_STR) or a JPy_JType + JPy_JType* targetTypeParsed; // the actual type that targetTypeArg is processed as + jboolean inst; + + PyObject* resultObj; + jobject objectRef; + + // Parse the 'args' from Python into 'obj'/'objType'. + if (!PyArg_ParseTuple(args, "OO:convert", &obj, &targetTypeArg)) { + return NULL; + } + + if (obj == Py_None) { + Py_RETURN_NONE; + } + + if (JPy_IS_STR(targetTypeArg)) { + const char* typeName = JPy_AS_UTF8(targetTypeArg); + targetTypeParsed = JType_GetTypeForName(jenv, typeName, JNI_FALSE); + if (targetTypeParsed == NULL) { + return NULL; + } + } else if (JType_Check(targetTypeArg)) { + targetTypeParsed = (JPy_JType*) targetTypeArg; + } else { + PyErr_SetString(PyExc_ValueError, "cast: argument 2 (obj_type) must be a Java type name or Java type object"); + return NULL; + } + + // If the input obj is a Java object, and it is already of the specified target type, + // then just cast it. + if (JObj_Check(obj)) { + inst = (*jenv)->IsInstanceOf(jenv, ((JPy_JObj*) obj)->objectRef, targetTypeParsed->classRef); + if (inst) { + return (PyObject*) JObj_FromType(jenv, (JPy_JType*) targetTypeParsed, ((JPy_JObj*) obj)->objectRef); + } + } + + // Convert the Python object to the specified Java type + if (JPy_AsJObjectWithType(jenv, obj, &objectRef, targetTypeParsed) < 0) { + return NULL; + } + + // Create a global reference for the objectRef (so it is valid after we exit this frame) + objectRef = (*jenv)->NewGlobalRef(jenv, objectRef); + if (objectRef == NULL) { + PyErr_NoMemory(); + return NULL; + } + + // Create a PyObject (JObj) to hold the result + resultObj = (JPy_JObj*) PyObject_New(JPy_JObj, JTYPE_AS_PYTYPE(targetTypeParsed)); + if (resultObj == NULL) { + (*jenv)->DeleteGlobalRef(jenv, objectRef); + return NULL; + } + // Store the reference to the converted object in the result JObj + ((JPy_JObj*) resultObj)->objectRef = objectRef; + + return (PyObject*) resultObj; +} + + +PyObject* JPy_convert(PyObject* self, PyObject* args) +{ + JPy_FRAME(PyObject*, NULL, JPy_convert_internal(jenv, self, args), 16) +} + PyObject* JPy_array_internal(JNIEnv* jenv, PyObject* self, PyObject* args) { JPy_JType* componentType; diff --git a/src/test/java/org/jpy/EmbeddableTestJunit.java b/src/test/java/org/jpy/EmbeddableTestJunit.java index 0f85afdf..876436b9 100644 --- a/src/test/java/org/jpy/EmbeddableTestJunit.java +++ b/src/test/java/org/jpy/EmbeddableTestJunit.java @@ -1,12 +1,18 @@ package org.jpy; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; /** * Wraps up {@link EmbeddableTest} with JUnit so {@link EmbeddableTest} doesn't have to have the * JUnit dependency. */ public class EmbeddableTestJunit { + + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); + @Test public void testStartingAndStoppingIfAvailable() { EmbeddableTest.testStartingAndStoppingIfAvailable(); diff --git a/src/test/java/org/jpy/JavaReflectionTest.java b/src/test/java/org/jpy/JavaReflectionTest.java index 6a0903da..ee935b96 100644 --- a/src/test/java/org/jpy/JavaReflectionTest.java +++ b/src/test/java/org/jpy/JavaReflectionTest.java @@ -19,7 +19,9 @@ import org.jpy.annotations.Mutable; import org.jpy.annotations.Return; import org.jpy.fixtures.MethodReturnValueTestFixture; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; @@ -34,6 +36,9 @@ */ public class JavaReflectionTest { + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); + @Test public void testPrimitiveAndVoidNames() throws Exception { assertEquals("boolean", Boolean.TYPE.getName()); diff --git a/src/test/java/org/jpy/LifeCycleTest.java b/src/test/java/org/jpy/LifeCycleTest.java index 9f0e0eb3..009f2edd 100644 --- a/src/test/java/org/jpy/LifeCycleTest.java +++ b/src/test/java/org/jpy/LifeCycleTest.java @@ -1,9 +1,15 @@ package org.jpy; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; public class LifeCycleTest { + + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); + private static final boolean ON_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows"); @Test diff --git a/src/test/java/org/jpy/PyLibTest.java b/src/test/java/org/jpy/PyLibTest.java index bac187a8..3aaffe65 100644 --- a/src/test/java/org/jpy/PyLibTest.java +++ b/src/test/java/org/jpy/PyLibTest.java @@ -27,10 +27,14 @@ import java.util.Map; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; public class PyLibTest { + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); @Before public void setUp() throws Exception { //PyLib.Diag.setFlags(PyLib.Diag.F_ERR); diff --git a/src/test/java/org/jpy/PyLibWithSysPathTest.java b/src/test/java/org/jpy/PyLibWithSysPathTest.java index 6ccd1dc6..e3a165d8 100644 --- a/src/test/java/org/jpy/PyLibWithSysPathTest.java +++ b/src/test/java/org/jpy/PyLibWithSysPathTest.java @@ -17,6 +17,7 @@ package org.jpy; import org.junit.*; +import org.junit.rules.TestRule; import java.io.File; import java.net.URI; @@ -27,6 +28,9 @@ public class PyLibWithSysPathTest { + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); + @Before public void setUp() throws Exception { diff --git a/src/test/java/org/jpy/PyModuleTest.java b/src/test/java/org/jpy/PyModuleTest.java index 9a2c678a..33fb6632 100644 --- a/src/test/java/org/jpy/PyModuleTest.java +++ b/src/test/java/org/jpy/PyModuleTest.java @@ -17,6 +17,7 @@ package org.jpy; import org.junit.*; +import org.junit.rules.TestRule; import java.io.File; @@ -29,6 +30,8 @@ */ public class PyModuleTest { + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); @Before public void setUp() throws Exception { //System.out.println("PyModuleTest: Current thread: " + Thread.currentThread()); diff --git a/src/test/java/org/jpy/PyObjectTest.java b/src/test/java/org/jpy/PyObjectTest.java index e9fa45b2..2253fae6 100644 --- a/src/test/java/org/jpy/PyObjectTest.java +++ b/src/test/java/org/jpy/PyObjectTest.java @@ -22,6 +22,7 @@ import java.util.regex.Pattern; import org.junit.*; import org.jpy.fixtures.Processor; +import org.junit.rules.TestRule; import java.io.File; import java.io.IOException; @@ -40,6 +41,9 @@ public class PyObjectTest { private PyModule SPECIAL_METHODS; + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); + @Before public void setUp() throws Exception { // System.out.println("PyModuleTest: Current thread: " + @@ -59,7 +63,7 @@ public void tearDown() throws Exception { PyLib.Diag.setFlags(PyLib.Diag.F_OFF); PyLib.stopPython(); } - + @Test(expected = IllegalArgumentException.class) public void testNullPointer() throws Exception { new PyObject(0); @@ -161,9 +165,9 @@ public void testLocals() throws Exception { assertNotNull(localMap.get("y")); assertNotNull(localMap.get("z")); - assertEquals(7, localMap.get("x")); - assertEquals(6, localMap.get("y")); - assertEquals(13, localMap.get("z")); + assertEquals((byte) 7, localMap.get("x")); + assertEquals((byte) 6, localMap.get("y")); + assertEquals((byte) 13, localMap.get("z")); } @Test @@ -628,7 +632,7 @@ public void hashNegativeOne() { PyObject result = obj.callMethod("__hash__"); assertTrue(result.isInt()); - assertEquals(-1, obj.getIntValue()); + assertEquals(-1, result.getIntValue()); } @Test diff --git a/src/test/java/org/jpy/PyProxyTest.java b/src/test/java/org/jpy/PyProxyTest.java index cd1a3d05..7d845f6d 100644 --- a/src/test/java/org/jpy/PyProxyTest.java +++ b/src/test/java/org/jpy/PyProxyTest.java @@ -8,13 +8,16 @@ import java.io.File; import java.util.regex.Pattern; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; + +import org.junit.*; +import org.junit.rules.TestRule; public class PyProxyTest { private PyModule MODULE; + @Rule + public TestRule testStatePrinter = new TestStatePrinter(); + @Before public void setUp() throws Exception { PyLib.startPython(new File("src/test/python/fixtures").getCanonicalPath()); diff --git a/src/test/java/org/jpy/TestStatePrinter.java b/src/test/java/org/jpy/TestStatePrinter.java new file mode 100644 index 00000000..0ee37491 --- /dev/null +++ b/src/test/java/org/jpy/TestStatePrinter.java @@ -0,0 +1,31 @@ +package org.jpy; + +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +import java.time.Instant; + +public class TestStatePrinter extends TestWatcher { + @Override + protected void starting(Description desc) { + System.out.println(Instant.now().toString() + " Starting test: " + desc.getClassName() + ": " + desc.getMethodName()); + } + + @Override + protected void succeeded(Description desc) { + System.out.println(Instant.now().toString() + " Passed test: " + desc.getClassName() + ": " + desc.getMethodName()); + } + + @Override + protected void failed(Throwable e, Description desc) { + System.err.println(Instant.now().toString() + " Failed test: " + desc.getClassName() + ": " + desc.getMethodName()); + } + + @Override + protected void finished(Description desc) { + // TODO: Seems like this makes the tests fail (w/ JVM crash) more reliably. need to figure out why. + // (Without the GC, the tests usually fail but sometimes pass. With it, they always fail.) + System.out.println(Instant.now().toString() + " Running GC after test: " + desc.getClassName() + ": " + desc.getMethodName()); + System.gc(); + } +} diff --git a/src/test/java/org/jpy/fixtures/TypeConversionTestFixture.java b/src/test/java/org/jpy/fixtures/TypeConversionTestFixture.java index 90864ef2..071af504 100644 --- a/src/test/java/org/jpy/fixtures/TypeConversionTestFixture.java +++ b/src/test/java/org/jpy/fixtures/TypeConversionTestFixture.java @@ -41,4 +41,8 @@ public String stringifyObjectArrayArg(Object[] arg) { public String stringifyStringArrayArg(String[] arg) { return stringifyArgs((Object) arg); } + + public boolean isSameObject(Object o1, Object o2) { + return o1 == o2; + } } diff --git a/src/test/python/jpy_exception_test.py b/src/test/python/jpy_exception_test.py index fb31a943..1e89b553 100644 --- a/src/test/python/jpy_exception_test.py +++ b/src/test/python/jpy_exception_test.py @@ -27,13 +27,27 @@ def test_ArrayIndexOutOfBoundsException(self): fixture = self.Fixture() self.assertEqual(fixture.throwAioobeIfIndexIsNotZero(0), 101) + def check_exception(ex_msg: str, expected_idx: int, expected_len: int): + """ + Verify an ArrayIndexOutOfBoundsException. The exception message can vary by JVM. + :param ex_msg: The exception message + :param expected_idx: The invalid index expected in the exception message + :param expected_len: The expected length of the array that we attempted to access + """ + valid_ex_msg_1 = f'java.lang.ArrayIndexOutOfBoundsException: {expected_idx}' + valid_ex_msg_2 = f'java.lang.ArrayIndexOutOfBoundsException: Index {expected_idx} out of bounds for length {expected_len}' + ex_msg_correct = (ex_msg == valid_ex_msg_1 or ex_msg == valid_ex_msg_2) + + self.assertTrue(ex_msg_correct, + f'Exception message \'{ex_msg}\' does not match expectations: either \'{valid_ex_msg_1}\' or \'{valid_ex_msg_2}\'') + with self.assertRaises(RuntimeError, msg='Java ArrayIndexOutOfBoundsException expected') as e: fixture.throwAioobeIfIndexIsNotZero(1) - self.assertEqual(str(e.exception), 'java.lang.ArrayIndexOutOfBoundsException: 1') + check_exception(str(e.exception), 1, 1) with self.assertRaises(RuntimeError, msg='Java ArrayIndexOutOfBoundsException expected') as e: fixture.throwAioobeIfIndexIsNotZero(-1) - self.assertEqual(str(e.exception), 'java.lang.ArrayIndexOutOfBoundsException: -1') + check_exception(str(e.exception), -1, 1) def test_RuntimeException(self): fixture = self.Fixture() @@ -82,7 +96,7 @@ def test_VerboseException(self): # self.hexdump(expected_message) # print [i for i in xrange(min(len(expected_message), len(actual_message))) if actual_message[i] != expected_message[i]] - self.assertEquals(actual_message, expected_message) + self.assertEqual(actual_message, expected_message) with self.assertRaises(RuntimeError) as e: fixture.throwNpeIfArgIsNullNested3(None) @@ -107,7 +121,7 @@ def test_VerboseException(self): # self.hexdump(expected_message) # print [i for i in xrange(min(len(expected_message), len(actual_message))) if actual_message[i] != expected_message[i]] - self.assertEquals(actual_message, expected_message) + self.assertEqual(actual_message, expected_message) jpy.VerboseExceptions.enabled = False diff --git a/src/test/python/jpy_overload_test.py b/src/test/python/jpy_overload_test.py index 42394fe8..ec5937d2 100644 --- a/src/test/python/jpy_overload_test.py +++ b/src/test/python/jpy_overload_test.py @@ -97,7 +97,7 @@ def test_stringAsNumber(self): def test_numbersAsNumber(self): fixture = self.Fixture() - self.assertEqual(fixture.join3(1, 2), 'Integer(1),Integer(2)') + self.assertEqual(fixture.join3(1, 2), 'Byte(1),Integer(2)') self.assertEqual(fixture.join3(1.1, 2), 'Double(1.1),Integer(2)') def test_numbersAsComparable(self): @@ -137,7 +137,8 @@ def test_varargs(self): self.assertEqual(fixture.joinChar("Prefix", 65, 66), 'String(Prefix),char[](A,B)') self.assertEqual(fixture.joinBoolean("Prefix", True, False), 'String(Prefix),boolean[](true,false)') - self.assertEqual(fixture.joinObjects("Prefix", True, "A String", 3), 'String(Prefix),Object[](Boolean(true),String(A String),Integer(3))') + self.assertEqual(fixture.joinObjects("Prefix", True, "A String", 3), 'String(Prefix),Object[](Boolean(true),String(A String),Byte(3))') + self.assertEqual(fixture.joinObjects("Prefix", True, 0, 127, 128, 32767, 32768, 2147483647, 2147483648), 'String(Prefix),Object[](Boolean(true),Byte(0),Byte(127),Short(128),Short(32767),Integer(32768),Integer(2147483647),Long(2147483648))') def test_fixedArity(self): fixture = self.Fixture() diff --git a/src/test/python/jpy_translation_test.py b/src/test/python/jpy_translation_test.py index fe4d3f37..541e8ada 100644 --- a/src/test/python/jpy_translation_test.py +++ b/src/test/python/jpy_translation_test.py @@ -28,7 +28,7 @@ def test_Translation(self): fixture = self.Fixture() thing = fixture.makeThing(7) self.assertEqual(thing.getValue(), 7) - self.assertEquals(repr(type(thing)), "") + self.assertEqual(repr(type(thing)), "") jpy.type_translations['org.jpy.fixtures.Thing'] = make_wrapper thing = fixture.makeThing(8) diff --git a/src/test/python/jpy_typeconv_test.py b/src/test/python/jpy_typeconv_test.py index 06a24611..20ae6a61 100644 --- a/src/test/python/jpy_typeconv_test.py +++ b/src/test/python/jpy_typeconv_test.py @@ -3,20 +3,30 @@ import jpyutil - jpyutil.init_jvm(jvm_maxmem='512M', jvm_classpath=['target/test-classes']) import jpy class TestTypeConversions(unittest.TestCase): + """ + This test covers type conversion, including: + + - Automatic conversions of Python values to Java when calling Java methods from Python + - jpy.cast() (See JPy_cast) + - jpy.convert() (See JPy_convert_internal / JType_ConvertPythonToJavaObject) + """ + def setUp(self): self.Fixture = jpy.get_type('org.jpy.fixtures.TypeConversionTestFixture') self.assertTrue('org.jpy.fixtures.TypeConversionTestFixture' in jpy.types) - def test_ToObjectConversion(self): + """ + Test automatic conversion of Python values to Java objects (when passing Python values as arguments to Java methods). + """ + fixture = self.Fixture() - self.assertEqual(fixture.stringifyObjectArg(12), 'Integer(12)') + self.assertEqual(fixture.stringifyObjectArg(12), 'Byte(12)') self.assertEqual(fixture.stringifyObjectArg(0.34), 'Double(0.34)') self.assertEqual(fixture.stringifyObjectArg('abc'), 'String(abc)') @@ -24,6 +34,261 @@ def test_ToObjectConversion(self): fixture.stringifyObjectArg(1 + 2j) self.assertEqual(str(e.exception), 'cannot convert a Python \'complex\' to a Java \'java.lang.Object\'') + def test_cast(self): + """ + Test casts of Java objects using jpy.cast() + """ + fixture = self.Fixture() + + # Create a test String: + my_jstr = jpy.get_type('java.lang.String')('testStr') + self.assertEqual(type(my_jstr).jclassname, 'java.lang.String') + self.assertEqual(fixture.stringifyObjectArg(my_jstr), 'String(testStr)') + + # Cast to String (this should be a no-op) + my_jcharseq1 = jpy.cast(my_jstr, 'java.lang.String') + self.assertTrue(fixture.isSameObject(my_jstr, my_jcharseq1)) # Should be same Java object... + # self.assertTrue(my_jcharseq1 is my_jstr) # and the same Python object. (But currently a new one is returned.) + self.assertEqual(type(my_jcharseq1).jclassname, 'java.lang.String') + self.assertEqual(fixture.stringifyObjectArg(my_jcharseq1), 'String(testStr)') + + # Cast to CharSequence (using class name, not explicit jpy.get_type()) + my_jcharseq1 = jpy.cast(my_jstr, 'java.lang.CharSequence') + self.assertTrue(fixture.isSameObject(my_jstr, my_jcharseq1)) # Should be same Java object... + self.assertFalse(my_jcharseq1 is my_jstr) # but a new Python object + self.assertEqual(type(my_jcharseq1).jclassname, 'java.lang.CharSequence') + self.assertEqual(fixture.stringifyObjectArg(my_jcharseq1), 'String(testStr)') + + # Cast to CharSequence (using explicit jpy.get_type()): + my_jcharseq2 = jpy.cast(my_jstr, jpy.get_type('java.lang.CharSequence')) + self.assertTrue(fixture.isSameObject(my_jstr, my_jcharseq2)) # Should be same Java object... + self.assertFalse(my_jcharseq2 is my_jstr) # but a new Python object + self.assertEqual(type(my_jcharseq2).jclassname, 'java.lang.CharSequence') + self.assertEqual(fixture.stringifyObjectArg(my_jcharseq2), 'String(testStr)') + + def test_convert_cast(self): + """ + Test casts of Java objects using jpy.convert() + """ + fixture = self.Fixture() + + # Create a test String: + my_jstr = jpy.get_type('java.lang.String')('testStr') + self.assertEqual(type(my_jstr).jclassname, 'java.lang.String') + self.assertEqual(fixture.stringifyObjectArg(my_jstr), 'String(testStr)') + + # Cast to String (this should be a no-op) + my_jcharseq1 = jpy.convert(my_jstr, 'java.lang.String') + self.assertTrue(fixture.isSameObject(my_jstr, my_jcharseq1)) # Should be same Java object... + # self.assertTrue(my_jcharseq1 is my_jstr) # and the same Python object. (But currently a new one is returned.) + self.assertEqual(type(my_jcharseq1).jclassname, 'java.lang.String') + self.assertEqual(fixture.stringifyObjectArg(my_jcharseq1), 'String(testStr)') + + # Cast to CharSequence (using class name, not explicit jpy.get_type()) + my_jcharseq1 = jpy.convert(my_jstr, 'java.lang.CharSequence') + self.assertTrue(fixture.isSameObject(my_jstr, my_jcharseq1)) # Should be same Java object... + self.assertFalse(my_jcharseq1 is my_jstr) # but a new Python object. + self.assertEqual(type(my_jcharseq1).jclassname, 'java.lang.CharSequence') + self.assertEqual(fixture.stringifyObjectArg(my_jcharseq1), 'String(testStr)') + + # Cast to CharSequence (using explicit jpy.get_type()): + my_jcharseq2 = jpy.convert(my_jstr, jpy.get_type('java.lang.CharSequence')) + self.assertTrue(fixture.isSameObject(my_jstr, my_jcharseq2)) # Should be same Java object... + self.assertFalse(my_jcharseq2 is my_jstr) # but a new Python object. + self.assertEqual(type(my_jcharseq2).jclassname, 'java.lang.CharSequence') + self.assertEqual(fixture.stringifyObjectArg(my_jcharseq2), 'String(testStr)') + + def test_convert_toBoxedPrimitive(self): + fixture = self.Fixture() + + # Convert Python values to boxed types explicitly (using jpy.get_type() to retrieve the boxed type): + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(65, jpy.get_type('java.lang.Character'))), + 'Character(A)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('java.lang.Byte'))), 'Byte(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('java.lang.Short'))), 'Short(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('java.lang.Integer'))), 'Integer(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('java.lang.Long'))), 'Long(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('java.lang.Float'))), 'Float(12.0)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('java.lang.Double'))), 'Double(12.0)') + + # Convert Python values to boxed types (using type name, not explicit jpy.get_type()) + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(65, 'java.lang.Character')), 'Character(A)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, 'java.lang.Byte')), 'Byte(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, 'java.lang.Short')), 'Short(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, 'java.lang.Integer')), 'Integer(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, 'java.lang.Long')), 'Long(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, 'java.lang.Float')), 'Float(12.0)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, 'java.lang.Double')), 'Double(12.0)') + + # Convert Python values to boxed types (using primitive type names — but they still get boxed): + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(65, jpy.get_type('char'))), 'Character(A)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('byte'))), 'Byte(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('short'))), 'Short(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('int'))), 'Integer(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('long'))), 'Long(12)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('float'))), 'Float(12.0)') + self.assertEqual(fixture.stringifyObjectArg(jpy.convert(12, jpy.get_type('double'))), 'Double(12.0)') + + def test_convert_toPrimitiveArray(self): + fixture = self.Fixture() + + # Convert Python values to arrays of primitive types + + target_type = type(jpy.array(jpy.get_type('char'), 0)) + jobj = jpy.convert([65, 66, 67], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'char[](A,B,C)') + + target_type = type(jpy.array(jpy.get_type('byte'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'byte[](12,13,14)') + + target_type = type(jpy.array(jpy.get_type('short'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'short[](12,13,14)') + + target_type = type(jpy.array(jpy.get_type('int'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'int[](12,13,14)') + + target_type = type(jpy.array(jpy.get_type('long'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'long[](12,13,14)') + + target_type = type(jpy.array(jpy.get_type('float'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'float[](12.0,13.0,14.0)') + + target_type = type(jpy.array(jpy.get_type('double'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'double[](12.0,13.0,14.0)') + + def test_convert_toBoxedPrimitiveArray(self): + fixture = self.Fixture() + + # Convert Python values to arrays of boxed types + + target_type = type(jpy.array(jpy.get_type('java.lang.Character'), 0)) + jobj = jpy.convert([65, 66, 67], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Character[](Character(A),Character(B),Character(C))') + + target_type = type(jpy.array(jpy.get_type('java.lang.Byte'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Byte[](Byte(12),Byte(13),Byte(14))') + + target_type = type(jpy.array(jpy.get_type('java.lang.Short'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Short[](Short(12),Short(13),Short(14))') + + target_type = type(jpy.array(jpy.get_type('java.lang.Integer'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Integer[](Integer(12),Integer(13),Integer(14))') + + target_type = type(jpy.array(jpy.get_type('java.lang.Long'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Long[](Long(12),Long(13),Long(14))') + + target_type = type(jpy.array(jpy.get_type('java.lang.Float'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Float[](Float(12.0),Float(13.0),Float(14.0))') + + target_type = type(jpy.array(jpy.get_type('java.lang.Double'), 0)) + jobj = jpy.convert([12, 13, 14], target_type) + self.assertEqual(type(jobj), target_type) + self.assertEqual(fixture.stringifyObjectArg(jobj), 'Double[](Double(12.0),Double(13.0),Double(14.0))') + + def test_convert_toJavaLangObject(self): + """ + Test converting values to java.lang.Object (and letting the jpy module determine what Java type to convert + them to). + """ + java_lang_object_type = jpy.get_type('java.lang.Object') + + jobj = jpy.convert('A', java_lang_object_type) + expected_type = jpy.get_type('java.lang.String') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).toString(), 'A') + + jobj = jpy.convert('ABCDE', java_lang_object_type) + expected_type = jpy.get_type('java.lang.String') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).toString(), 'ABCDE') + + jobj = jpy.convert(True, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Boolean') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).booleanValue(), True) + + jobj = jpy.convert(False, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Boolean') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).booleanValue(), False) + + jobj = jpy.convert(12, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Byte') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).byteValue(), 12) + + jobj = jpy.convert(129, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Short') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).shortValue(), 129) + + jobj = jpy.convert(100_000, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Integer') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).intValue(), 100_000) + + jobj = jpy.convert(10_000_000_000, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Long') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).longValue(), 10_000_000_000) + + jobj = jpy.convert(123.45, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Double') # TODO: these go to Double, not Float ? + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).doubleValue(), 123.45) + + too_big_for_float = jpy.get_type('java.lang.Double').MAX_VALUE + jobj = jpy.convert(too_big_for_float, java_lang_object_type) + expected_type = jpy.get_type('java.lang.Double') + self.assertTrue(type(jobj).jclass.equals(java_lang_object_type.jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(expected_type.jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, expected_type).doubleValue(), too_big_for_float) + + def test_convert_toString(self): + jobj = jpy.convert('test string', jpy.get_type('java.lang.Object')) + self.assertTrue(type(jobj).jclass.equals(jpy.get_type('java.lang.Object').jclass), f'Type is {type(jobj)}') + self.assertTrue(jobj.getClass().equals(jpy.get_type('java.lang.String').jclass), f'Type is {jobj.getClass()}') + self.assertEqual(jpy.cast(jobj, jpy.get_type('java.lang.String')).toString(), 'test string') + + # Invalid conversion: + with self.assertRaises(ValueError) as e: + jpy.convert(12, jpy.get_type('java.lang.String')) + actual_message = str(e.exception) + expected_message = "cannot convert a Python 'int' to a Java 'java.lang.String'" + self.assertEqual(actual_message, expected_message) def test_ToPrimitiveArrayConversion(self): fixture = self.Fixture() @@ -44,12 +309,11 @@ def test_ToPrimitiveArrayConversion(self): fixture.stringifyIntArrayArg(1 + 2j) self.assertEqual(str(e.exception), 'no matching Java method overloads found') - def test_ToObjectArrayConversion(self): fixture = self.Fixture() - self.assertEqual(fixture.stringifyObjectArrayArg(('A', 12, 3.4)), 'Object[](String(A),Integer(12),Double(3.4))') - self.assertEqual(fixture.stringifyObjectArrayArg(['A', 12, 3.4]), 'Object[](String(A),Integer(12),Double(3.4))') + self.assertEqual(fixture.stringifyObjectArrayArg(('A', 12, 3.4)), 'Object[](String(A),Byte(12),Double(3.4))') + self.assertEqual(fixture.stringifyObjectArrayArg(['A', 12, 3.4]), 'Object[](String(A),Byte(12),Double(3.4))') self.assertEqual(fixture.stringifyStringArrayArg(('A', 'B', 'C')), 'String[](String(A),String(B),String(C))') self.assertEqual(fixture.stringifyStringArrayArg(['A', 'B', 'C']), 'String[](String(A),String(B),String(C))') diff --git a/src/test/python/jpy_typeconv_test_pyobj.py b/src/test/python/jpy_typeconv_test_pyobj.py new file mode 100644 index 00000000..3b7bab76 --- /dev/null +++ b/src/test/python/jpy_typeconv_test_pyobj.py @@ -0,0 +1,61 @@ +import unittest + +import jpyutil + +jpyutil.init_jvm(jvm_maxmem='512M', jvm_classpath=['target/classes']) +import jpy + + +class TestTypeConversionsPyObj(unittest.TestCase): + """ + This test covers explicitly converting Python objects to Java PyObject instances. It is separate from + jpy_typeconv_test.py because it requires a different classpath. + """ + + def test_convert_toPyObject(self): + # Note that this test requires jvm_classpath=['target/classes'] (not jvm_classpath=['target/***test-***classes'] + + print('Starting test_convert_toPyObject') + PyObject_type = jpy.get_type('org.jpy.PyObject') + print('test_convert_toPyObject: Got type for PyObject') + + print('test_convert_toPyObject: Testing value: \'A\'') + print('test_convert_toPyObject: Doing first conversion') + val = 'A' + conv = jpy.convert(val, PyObject_type) + print('test_convert_toPyObject: Getting first pointer') + ptr = conv.getPointer() + print('test_convert_toPyObject: Got first pointer') + self.assertEqual(ptr, id(val)) + print('test_convert_toPyObject: Passed first assertion') + + print('test_convert_toPyObject: Testing value: string') + val = 'ABCDE' + self.assertEqual(jpy.convert(val, PyObject_type).getPointer(), id(val)) + + print('test_convert_toPyObject: Testing value: True') + val = True + self.assertEqual(jpy.convert(val, PyObject_type).getPointer(), id(val)) + + print('test_convert_toPyObject: Testing value: False') + val = False + self.assertEqual(jpy.convert(val, PyObject_type).getPointer(), id(val)) + + print('test_convert_toPyObject: Testing value: 12') + val = 12 + self.assertEqual(jpy.convert(val, PyObject_type).getPointer(), id(val)) + + print('test_convert_toPyObject: Testing value: 12.2') + val = 12.2 + self.assertEqual(jpy.convert(val, PyObject_type).getPointer(), id(val)) + + print('test_convert_toPyObject: Testing value: [1, 2.0, "ABCDE"]') + val = [1, 2.0, "ABCDE"] + self.assertEqual(jpy.convert(val, PyObject_type).getPointer(), id(val)) + + print('Finished test_convert_toPyObject') + + +if __name__ == '__main__': + print('\nRunning ' + __file__) + unittest.main()