Skip to content

Commit

Permalink
PyJType for java.util.Map now extends collections.abc.Mapping
Browse files Browse the repository at this point in the history
This allows Java maps to be used in python anywhere a python mapping
is expected. Before this change java Maps are missing functionality
such as item(), keys(), and values() which are now automatically
added by extending collections.abc.Mapping.
  • Loading branch information
bsteffensmeier committed Apr 30, 2023
1 parent f7a8514 commit cc7222e
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 52 deletions.
18 changes: 17 additions & 1 deletion src/main/c/Objects/pyjtype.c
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,25 @@ static int populateCustomTypeDict(JNIEnv *env, PyObject* fqnToPyType)
pyjcollection)) {
return -1;
}
if (!addSpecToTypeDict(env, fqnToPyType, JMAP_TYPE, &PyJMap_Spec, NULL)) {
PyObject* collectionAbc = PyImport_ImportModule("collections.abc");
if (!collectionAbc) {
return -1;
}
PyObject* mapping = PyObject_GetAttrString(collectionAbc, "Mapping");
if (!mapping) {
Py_DECREF(collectionAbc);
return -1;
}
Py_DECREF(collectionAbc);
if (!PyType_Check(mapping)) {
Py_DECREF(mapping);
return -1;
}
if (!addSpecToTypeDict(env, fqnToPyType, JMAP_TYPE, &PyJMap_Spec, (PyTypeObject*) mapping)) {
Py_DECREF(mapping);
return -1;
}
Py_DECREF(mapping);
if (!addSpecToTypeDict(env, fqnToPyType, JNUMBER_TYPE, &PyJNumber_Spec,
&PyJObject_Type)) {
return -1;
Expand Down
65 changes: 14 additions & 51 deletions src/test/python/test_convert.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import unittest
import jep
from datetime import datetime
from java.util import Calendar, Date, HashMap, Map, TimeZone, TreeMap

def map_to_dict(jmap):
result = {}
for entry in jmap.entrySet():
result[entry.getKey()] = entry.getValue()
return result
from java.sql import Time
from java.util import Calendar, Date, TimeZone

def date_to_datetime(jdate):
return datetime.utcfromtimestamp(jdate.getTime()/1000)
Expand All @@ -18,39 +13,6 @@ def setUp(self):
Test = jep.findClass('jep.test.Test')
self.javaPassThrough = Test().testObjectPassThrough

def test_j2p_converter_map(self):
# Register a conversion function, all Maps coming from java are converted to dicts
jep.setJavaToPythonConverter(Map, map_to_dict)
# Constructors do not convert so construct a java Map for testing.
jmap = HashMap()
jmap.put("key1","value1")
jmap.put("key2","value2")

# The passthrough is a java method that returns whatever it is given so the resulting
# value is automatically passed through the converter by jep.
after = self.javaPassThrough(jmap)
# The result is not a java Map
self.assertNotIsInstance(after, Map)
# It is a python Dict
self.assertIsInstance(after, dict)
# a Map is not equal to a dict
self.assertNotEqual(jmap, after)
# But it has items() which a Map does not
self.assertTrue(hasattr(after,"items"))
# The contents of the dict match what you would expect from a map
self.assertEqual(len(jmap), len(after))
self.assertIn(("key1", "value1"), after.items())
self.assertIn(("key2", "value2"), after.items())

# Remove the converter to return to the default behavior
jep.setJavaToPythonConverter(Map, None)
after = self.javaPassThrough(jmap)
# With no converter the result is a Map, not a dict
self.assertIsInstance(after, Map)
self.assertNotIsInstance(after, dict)
self.assertEqual(jmap, after)
self.assertFalse(hasattr(after,"items"))

def test_j2p_converter_date(self):
cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"))
cal.clear() # Set to January 1, 1970 00:00:00.000 GMT
Expand Down Expand Up @@ -86,18 +48,19 @@ def test_j2p_converter_date(self):
self.assertIsInstance(after.getSeconds(), int)

def test_j2p_converter_override(self):
# Convert all Maps, except TreeMaps
jep.setJavaToPythonConverter(Map, map_to_dict)
jep.setJavaToPythonConverter(TreeMap, lambda x: x)
after = self.javaPassThrough(HashMap())
self.assertNotIsInstance(after, Map)
self.assertIsInstance(after, dict)
after = self.javaPassThrough(TreeMap())
self.assertIsInstance(after, Map)
self.assertNotIsInstance(after, dict)
# Convert Dates, not Times
jep.setJavaToPythonConverter(Date, date_to_datetime)
jep.setJavaToPythonConverter(Time, lambda x: x)
after = self.javaPassThrough(Date())
self.assertNotIsInstance(after, Date)
self.assertIsInstance(after, datetime)
after = self.javaPassThrough(Time(1))
self.assertIsInstance(after, Date)
self.assertNotIsInstance(after, datetime)
jep.setJavaToPythonConverter(Date, None)
jep.setJavaToPythonConverter(Time, None)

def tearDown(self):
# make sure converters are deregistered since it shouldn't affect other tests.
jep.setJavaToPythonConverter(Map, None)
jep.setJavaToPythonConverter(Date, None)
jep.setJavaToPythonConverter(TreeMap, None)
jep.setJavaToPythonConverter(Time, None)
8 changes: 8 additions & 0 deletions src/test/python/test_maps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import unittest

from collections.abc import Mapping

from java.util import HashMap


Expand Down Expand Up @@ -41,6 +43,12 @@ def test_len(self):
pymap = makePythonDict()
self.assertEqual(len(jmap), len(pymap))

def test_mapping(self):
jmap = makeJavaMap()
self.assertTrue(isinstance(jmap, Mapping))
pymap = makePythonDict()
self.assertEqual(dict(jmap), pymap)

def test_del(self):
jmap = makeJavaMap()
pymap = makePythonDict()
Expand Down

0 comments on commit cc7222e

Please sign in to comment.