Skip to content

Commit 4b7c37a

Browse files
authored
Add support for additional python types as dict keys in Struct.to_json (#321)
* Add support for additional python types as dict keys in Struct.to_json Signed-off-by: Arham Chopra <[email protected]> * Fix memory leaks, add docs, and address comments Signed-off-by: Arham Chopra <[email protected]> --------- Signed-off-by: Arham Chopra <[email protected]>
1 parent f86bac1 commit 4b7c37a

File tree

3 files changed

+125
-7
lines changed

3 files changed

+125
-7
lines changed

cpp/csp/python/PyStructToJson.cpp

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,40 @@ rapidjson::Value toJsonRecursive( const StructPtr& self, rapidjson::Document& do
198198
return new_dict;
199199
}
200200

201-
rapidjson::Value pyDictKeyToName( PyObject * py_key, rapidjson::Document& doc )
201+
rapidjson::Value pyDictKeyToName( PyObject * py_key, rapidjson::Document& doc, PyObject * callable )
202202
{
203-
// Only support strings, ints, and floats as keys
203+
// NOTE: Only support None, bool, strings, ints, and floats, date, time, datetime, enums, csp.Enums as keys
204204
// JSON encoding requires all names to be strings so convert them to strings
205+
206+
static thread_local PyTypeObjectPtr s_tl_enum_type;
207+
// Get the enum type on the first call and save it for future use
208+
if( s_tl_enum_type.get() == nullptr ) [[unlikely]]
209+
{
210+
// Import enum module to extract the Enum type
211+
auto py_enum_module = PyObjectPtr::own( PyImport_ImportModule( "enum" ) );
212+
if( py_enum_module.get() )
213+
{
214+
s_tl_enum_type = PyTypeObjectPtr::own( reinterpret_cast<PyTypeObject*>( PyObject_GetAttrString( py_enum_module.get(), "Enum" ) ) );
215+
}
216+
else
217+
{
218+
CSP_THROW( RuntimeException, "Unable to import enum module from the python standard library" );
219+
}
220+
}
221+
205222
rapidjson::Value val;
206-
if( PyUnicode_Check( py_key ) )
223+
if( py_key == Py_None )
224+
{
225+
val.SetString( "null" );
226+
}
227+
else if( PyBool_Check( py_key ) )
228+
{
229+
auto str_obj = PyObjectPtr::own( PyObject_Str( py_key ) );
230+
Py_ssize_t len = 0;
231+
const char * str = PyUnicode_AsUTF8AndSize( str_obj.get(), &len );
232+
val.SetString( str, len, doc.GetAllocator() );
233+
}
234+
else if( PyUnicode_Check( py_key ) )
207235
{
208236
Py_ssize_t len;
209237
auto str = PyUnicode_AsUTF8AndSize( py_key, &len );
@@ -220,9 +248,8 @@ rapidjson::Value pyDictKeyToName( PyObject * py_key, rapidjson::Document& doc )
220248
auto json_obj = doubleToJson( key, doc );
221249
if ( json_obj.IsNull() )
222250
{
223-
auto * str_obj = PyObject_Str( py_key );
224-
Py_ssize_t len = 0;
225-
const char * str = PyUnicode_AsUTF8AndSize( str_obj, &len );
251+
auto str_obj = PyObjectPtr::own( PyObject_Str( py_key ) );
252+
const char * str = PyUnicode_AsUTF8( str_obj.get() );
226253
CSP_THROW( ValueError, "Cannot serialize " + std::string( str ) + " to key in JSON" );
227254
}
228255
else
@@ -233,6 +260,35 @@ rapidjson::Value pyDictKeyToName( PyObject * py_key, rapidjson::Document& doc )
233260
val.SetString( s.str(), doc.GetAllocator() );
234261
}
235262
}
263+
else if( PyTime_CheckExact( py_key ) )
264+
{
265+
auto v = fromPython<Time>( py_key );
266+
val = toJson( v, CspType( CspType::Type::TIME ), doc, callable );
267+
}
268+
else if( PyDate_CheckExact( py_key ) )
269+
{
270+
auto v = fromPython<Date>( py_key );
271+
val = toJson( v, CspType( CspType::Type::DATE ), doc, callable );
272+
}
273+
else if( PyDateTime_CheckExact( py_key ) )
274+
{
275+
auto v = fromPython<DateTime>( py_key );
276+
val = toJson( v, CspType( CspType::Type::DATETIME ), doc, callable );
277+
}
278+
else if( PyType_IsSubtype( Py_TYPE( py_key ), &PyCspEnum::PyType ) )
279+
{
280+
auto enum_ptr = static_cast<PyCspEnum *>( py_key ) -> enum_;
281+
val = toJson( enum_ptr, CspType( CspType::Type::ENUM ), doc, callable );
282+
}
283+
else if( PyType_IsSubtype( Py_TYPE( py_key ), s_tl_enum_type.get() ) )
284+
{
285+
// Use the `name` attribute of the enum for the string representation
286+
auto py_enum_name = PyObjectPtr::own( PyObject_GetAttrString( py_key, "name" ) );
287+
auto str_obj = PyObjectPtr::own( PyObject_Str( py_enum_name.get() ) );
288+
Py_ssize_t len = 0;
289+
const char * str = PyUnicode_AsUTF8AndSize( str_obj.get(), &len );
290+
val.SetString( str, len, doc.GetAllocator() );
291+
}
236292
else
237293
{
238294
CSP_THROW( ValueError, "Cannot serialize key of type: " + std::string( Py_TYPE( py_key ) -> tp_name ) );
@@ -284,7 +340,7 @@ rapidjson::Value pyDictToJson( PyObject * py_dict, rapidjson::Document& doc, PyO
284340

285341
while( PyDict_Next( py_dict, &pos, &py_key, &py_value ) )
286342
{
287-
auto key = pyDictKeyToName( py_key, doc );
343+
auto key = pyDictKeyToName( py_key, doc, callable );
288344
auto res = pyObjectToJson( py_value, doc, callable, false );
289345
new_dict.AddMember( key, res, doc.GetAllocator() );
290346
}

csp/tests/impl/test_struct.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
import json
23
import numpy as np
34
import pickle
@@ -1742,6 +1743,7 @@ class MyStruct(csp.Struct):
17421743
result_dict = {"i": 456, "d_any": d_any_res}
17431744
self.assertEqual(json.loads(test_struct.to_json()), result_dict)
17441745

1746+
# Special floats not supported as keys
17451747
d_f = {float("nan"): 2, 2.3: 4, 3.4: 6, 4.5: 7}
17461748
d_f_res = {str(k): v for k, v in d_f.items()}
17471749
test_struct = MyStruct(i=456, d_any=d_f)
@@ -1760,6 +1762,65 @@ class MyStruct(csp.Struct):
17601762
with self.assertRaises(ValueError):
17611763
test_struct.to_json()
17621764

1765+
# None as key
1766+
d_none = {
1767+
None: 2,
1768+
}
1769+
d_none_res = {"null": 2}
1770+
test_struct = MyStruct(i=456, d_any=d_none)
1771+
result_dict = {"i": 456, "d_any": d_none_res}
1772+
self.assertEqual(json.loads(test_struct.to_json()), result_dict)
1773+
1774+
# Bool as key
1775+
d_bool = {True: 2, False: "abc"}
1776+
d_bool_res = {str(k): v for k, v in d_bool.items()}
1777+
test_struct = MyStruct(i=456, d_any=d_bool)
1778+
result_dict = {"i": 456, "d_any": d_bool_res}
1779+
self.assertEqual(json.loads(test_struct.to_json()), result_dict)
1780+
1781+
# Datetime as key
1782+
dt = datetime.now(tz=pytz.utc)
1783+
d_datetime = {dt: "datetime"}
1784+
test_struct = MyStruct(i=456, d_any=d_datetime)
1785+
result_dict = json.loads(test_struct.to_json())
1786+
self.assertEqual({datetime.fromisoformat(k): v for k, v in result_dict["d_any"].items()}, d_datetime)
1787+
1788+
dt = datetime.now(tz=pytz.utc)
1789+
d_datetime = {dt.date(): "date"}
1790+
test_struct = MyStruct(i=456, d_any=d_datetime)
1791+
result_dict = json.loads(test_struct.to_json())
1792+
self.assertEqual({date.fromisoformat(k): v for k, v in result_dict["d_any"].items()}, d_datetime)
1793+
1794+
dt = datetime.now(tz=pytz.utc)
1795+
d_datetime = {dt.time(): "time"}
1796+
test_struct = MyStruct(i=456, d_any=d_datetime)
1797+
result_dict = json.loads(test_struct.to_json())
1798+
self.assertEqual({time.fromisoformat(k): v for k, v in result_dict["d_any"].items()}, d_datetime)
1799+
1800+
# csp.Enum as key
1801+
class MyCspEnum(csp.Enum):
1802+
KEY1 = csp.Enum.auto()
1803+
KEY2 = csp.Enum.auto()
1804+
KEY3 = csp.Enum.auto()
1805+
1806+
d_csp_enum = {MyCspEnum.KEY1: "key1", MyCspEnum.KEY2: "key2", MyCspEnum.KEY3: "key3"}
1807+
d_csp_enum_res = {k.name: v for k, v in d_csp_enum.items()}
1808+
test_struct = MyStruct(i=456, d_any=d_csp_enum)
1809+
result_dict = {"i": 456, "d_any": d_csp_enum_res}
1810+
self.assertEqual(json.loads(test_struct.to_json()), result_dict)
1811+
1812+
# enum as key
1813+
class MyPyEnum(enum.Enum):
1814+
KEY1 = enum.auto()
1815+
KEY2 = enum.auto()
1816+
KEY3 = enum.auto()
1817+
1818+
d_py_enum = {MyPyEnum.KEY1: "key1", MyPyEnum.KEY2: "key2", MyPyEnum.KEY3: "key3"}
1819+
d_py_enum_res = {k.name: v for k, v in d_csp_enum.items()}
1820+
test_struct = MyStruct(i=456, d_any=d_py_enum)
1821+
result_dict = {"i": 456, "d_any": d_py_enum_res}
1822+
self.assertEqual(json.loads(test_struct.to_json()), result_dict)
1823+
17631824
def test_to_json_struct(self):
17641825
class MySubSubStruct(csp.Struct):
17651826
b: bool = True

docs/wiki/api-references/csp.Struct-API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ print(f"Using FastList field: value {s.a}, type {type(s.a)}, is Python list: {is
142142
- **`from_dict(self, dict)`**: convert a regular python dict to an instance of the struct
143143
- **`metadata(self)`**: returns the struct's metadata as a dictionary of key : type pairs
144144
- **`to_dict(self)`**: convert struct instance to a python dictionary
145+
- **`to_json(self, callback=lambda x: x)`**: convert struct instance to a json string, callback is invoked for any values encountered when processing the struct that are not basic Python types, datetime types, tuples, lists, dicts, csp.Structs, or csp.Enums. The callback should convert the unhandled type to a combination of the known types.
145146
- **`all_fields_set(self)`**: returns `True` if all the fields on the struct are set. Note that this will not recursively check sub-struct fields
146147

147148
# Note on inheritance

0 commit comments

Comments
 (0)