-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Function for converting Python values to an explicit Java type #128
Function for converting Python values to an explicit Java type #128
Conversation
…for extracting a JPy_JObject's declared type.
…nd as_jobj_internal.
# Conflicts: # jpyutil.py
src/main/c/jpy_module.c
Outdated
@@ -57,10 +59,19 @@ static PyMethodDef JPy_Functions[] = { | |||
"get_type(name, resolve=True) - Return the Java class with the given name, e.g. 'java.io.File'. " | |||
"Loads the Java class from the JVM if not already done. Optionally avoids resolving the class' methods."}, | |||
|
|||
{"get_type_name", (PyCFunction) JPy_get_type_name, METH_VARARGS, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this? You can currently do type(jobj).jclass.getName()
. This does involve a boundary crossing though.
If we think it's important to have w/o boundary crossing, and explicitly meant for jobj types, instead of adding jpy
top level method, we can attach an attribute to the JPy_JType. From end user perspective, it would look something like:
# Sources from JPy_JType->javaName
type(jobj).jclassname
would be appropriate. See logic in JType_AddClassAttribute
.
I'm actually surprised that
type(jobj).__name__
isn't the full classname; https://docs.python.org/3/c-api/typeobj.html#tp-slots hints that that should be tp_name.
And our code has this:
typedef struct JPy_JType
{
...
// typeObj.tp_name is this type's fully qualified Java name (same as 'javaName' field).
PyTypeObject typeObj;
// The Java type name.
char* javaName;
Something screwy is going on though:
>>> jpy.get_type('java.lang.CharSequence').__name__
'CharSequence'
>>> jpy.get_type('java.math.BigInteger').__name__
'BigInteger'
I'll do some further digging, b/c this seems odd...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_name
Looks like
>>> f"{type.__module__}.{type.__name}"
is the way to get the java class name; this doesn't quite work for primitives (not sure what we expect to happen w/ primitives).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think primitives apply — I don't think you can wrap an unboxed primitive value in a JPy_JObj
.
I got rid of this method and added type(obj).jclassname
.
src/main/c/jpy_module.c
Outdated
if (resultObj == NULL) { | ||
return NULL; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should delete the global ref if this happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks — fixed
src/main/c/jpy_module.c
Outdated
{"cast", JPy_cast, METH_VARARGS, | ||
"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."}, | ||
|
||
{"as_jobj", JPy_as_jobj, METH_VARARGS, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cool - previously, we could do this, it just required explicit java code:
public class MyClass {
public static MyJavaType identity(MyJavaType x) {
return x;
}
}
jobj = jpy.get_type('MyClass').identity(pobj)
I'm assuming these are the semantics you are after?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, basically I had two parallel lists, one of java types and one of python values (in practice strings/numbers), and I wanted to stick the python values into a java Object[]
with the given Java type, but numbers I wanted as Long
were getting converted to Byte
. I could have just used the constructors explicitly but that's not as clean, and it seems worthwhile to expose JType_ConvertPythonToJavaObject
anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not too crazy about the name as_jobj
, would conv
be better?
From what I can gather, I do like that I can now create a org.jpy.PyObject in Python directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm definitely open to changing it. I went with as_jobj
because it ultimately calls JPy_AsJObjectWithType
, and I thought having 'obj' in the name explicitly could be helpful because conversion in Java is a distinct concept (though a related one) that also involves Java primitives.
@devinrsmith what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "as" gives off the wrong semantic notion, somewhat akin to zero-cost. While the same could be said about JPy_AsJObjectWithType
, that's lower level and not exposed to the end user. Also, it calls down into JType_ConvertPythonToJavaObject
.
I'm also not the biggest fan of trying to save a few characters; my vote would be for jpy.convert
instead of jpy.conv
/ jpy.as_jobj
.
src/test/python/jpy_typeconv_test.py
Outdated
self.assertEqual(fixture.stringifyObjectArg(my_jcharseq2), 'String(testStr)') | ||
|
||
|
||
def test_ToObjectConversionTyped(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think trying to exercise some of the other logic from JType_ConvertPythonToJavaObject would be nice; arrays, boolean, char, PyObject, String.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added more tests. The test for converting explicitly to PyObject
doesn't run, apparently due to an issue with the classpath
…n why it doesn't work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think your changes to the JVOID / JNULL macros match with their indented use cases. While technically the results will be the same, the usage semantics are wrong. I would revert all these changes.
As you've probably noticed, the jpy tests aren't very robust. Ideally, we'd get them to run in GH CI here, but that hasn't been done yet.
} 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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you comment on the effect of this? It looks like you removed the JPy_JObject condition from Long / Float, but not Double?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the cases I changed were:
} 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)))) {
return JType_CreateJavaLongObject(jenv, type, pyArg, objectRef);
and:
} 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)))) {
return JType_CreateJavaFloatObject(jenv, type, pyArg, objectRef);
}
in both case, the second type == JPy_JObject
test will always be false — for the first case, we will have already landed in JType_CreateJavaNumberFromPythonInt
and in the second case we'd hit JType_CreateJavaDoubleObject
.
src/main/c/jpy_module.c
Outdated
@@ -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(""); | |||
return JPy_FROM_JVOID(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JPy_FROM_JVOID
is meant to be used in combination with void.class
type; it's not appropriate to use it in these contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced the handful of Py_BuildValue()
s I changed with a new JPy_PY_NONE()
src/main/c/jpy_module.c
Outdated
return NULL; | ||
} | ||
|
||
if (obj == Py_None) { | ||
return Py_BuildValue(""); | ||
return JPy_FROM_JNULL(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not appropriate to use here unless we are converting from a null java object.
src/main/c/jpy_module.c
Outdated
} else { | ||
return Py_BuildValue(""); | ||
return JPy_FROM_JNULL(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
*/ | ||
// System.out.println(obj.getObjectValue()); | ||
|
||
// assertEquals(-1, obj.getIntValue()); // TODO: should this be 'result.getIntValue()'? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes; this should be result.getIntValue()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks — updated & reenabled the test
src/test/python/jpy_typeconv_test.py
Outdated
|
||
def test_AsJobjToPyObject(self): | ||
print('Starting test_AsJobjToPyObject') | ||
# TODO: "Java class 'org.jpy.PyObject' not found"??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense; PyObject is only necessary if the user needs to reference python from java. At least with this test scaffolding, the jpy jar is not on the classpath.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok — I moved this to a separate file (jpy_typeconv_test_pyobj.py
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Adding Jianfeng for final pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes LGTM, not sure if I like the name very much.
src/main/c/jpy_module.c
Outdated
{"cast", JPy_cast, METH_VARARGS, | ||
"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."}, | ||
|
||
{"as_jobj", JPy_as_jobj, METH_VARARGS, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not too crazy about the name as_jobj
, would conv
be better?
From what I can gather, I do like that I can now create a org.jpy.PyObject in Python directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am happy with the name change.
cast
as_jobj
method to convert a Python value to a Java object of the given type. This leverages the existingJPy_AsJObjectWithType
method.Byte
instead ofInteger
)hashNegativeOne
testreturn Py_NONE
s withPy_RETURN_NONE
(for correct reference counting)