Skip to content

Commit

Permalink
Function for converting Python values to an explicit Java type (#128)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
rbasralian authored Jan 24, 2024
1 parent 00a61c4 commit 1331a91
Show file tree
Hide file tree
Showing 26 changed files with 556 additions and 56 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Vagrantfile
*.so
*.dll

.vscode/
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------

Expand Down
5 changes: 4 additions & 1 deletion doc/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
4 changes: 2 additions & 2 deletions src/main/c/jni/org_jpy_PyLib.c
Original file line number Diff line number Diff line change
Expand Up @@ -2611,15 +2611,15 @@ 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)
{
if (stdout != NULL) {
fflush(stdout);
}
return Py_BuildValue("");
Py_RETURN_NONE;
}

static PyMethodDef JPrint_Functions[] = {
Expand Down
4 changes: 2 additions & 2 deletions src/main/c/jpy_conv.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions src/main/c/jpy_conv.h
Original file line number Diff line number Diff line change
Expand Up @@ -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()


/**
Expand Down
6 changes: 3 additions & 3 deletions src/main/c/jpy_jmethod.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}


Expand Down
4 changes: 2 additions & 2 deletions src/main/c/jpy_jobj.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 8 additions & 7 deletions src/main/c/jpy_jtype.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
}

Expand Down
109 changes: 93 additions & 16 deletions src/main/c/jpy_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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'."},
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -551,41 +557,41 @@ 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)) {
PyErr_SetString(PyExc_ValueError, "cast: argument 1 (obj) must be a Java object");
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;
}
}

Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/org/jpy/EmbeddableTestJunit.java
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/org/jpy/JavaReflectionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +36,9 @@
*/
public class JavaReflectionTest {

@Rule
public TestRule testStatePrinter = new TestStatePrinter();

@Test
public void testPrimitiveAndVoidNames() throws Exception {
assertEquals("boolean", Boolean.TYPE.getName());
Expand Down
Loading

0 comments on commit 1331a91

Please sign in to comment.