Skip to content

Commit 4911073

Browse files
daiyippyglove authors
authored andcommitted
pg.to_json to support opaque object serialization with pickle.
PiperOrigin-RevId: 559962280
1 parent e10d481 commit 4911073

File tree

6 files changed

+85
-17
lines changed

6 files changed

+85
-17
lines changed

pyglove/core/object_utils/json_conversion.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import importlib
2020
import inspect
2121
import marshal
22+
import pickle
2223
import types
2324
import typing
2425
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union
@@ -199,8 +200,8 @@ def register(
199200
Args:
200201
type_name: A global unique string identifier for subclass.
201202
subclass: A subclass of JSONConvertible.
202-
override_existing: If True, registering an type name is allowed.
203-
Otherwise an error will be raised.
203+
override_existing: If True, override the class if the type name is
204+
already present in the registry. Otherwise an error will be raised.
204205
"""
205206
cls._TYPE_REGISTRY.register(type_name, subclass, override_existing)
206207

@@ -246,6 +247,51 @@ def __init_subclass__(cls):
246247
JSONConvertible.register(type_name, cls, override_existing=True)
247248

248249

250+
def _type_name(type_or_function: Union[Type[Any], types.FunctionType]) -> str:
251+
"""Returns the ID for a type or function."""
252+
return f'{type_or_function.__module__}.{type_or_function.__qualname__}'
253+
254+
255+
class _OpaqueObject(JSONConvertible):
256+
"""An JSON converter for opaque Python objects."""
257+
258+
def __init__(self, value: Any, encoded: bool = False):
259+
if encoded:
260+
value = self.decode(value)
261+
self._value = value
262+
263+
@property
264+
def value(self) -> Any:
265+
"""Returns the decoded value."""
266+
return self._value
267+
268+
def encode(self, value: Any) -> JSONValueType:
269+
try:
270+
return base64.encodebytes(pickle.dumps(value)).decode('utf-8')
271+
except Exception as e:
272+
raise ValueError(
273+
f'Cannot encode opaque object {value!r} with pickle.') from e
274+
275+
def decode(self, json_value: JSONValueType) -> Any:
276+
assert isinstance(json_value, str), json_value
277+
try:
278+
return pickle.loads(base64.decodebytes(json_value.encode('utf-8')))
279+
except Exception as e:
280+
raise ValueError('Cannot decode opaque object with pickle.') from e
281+
282+
def to_json(self, **kwargs) -> JSONValueType:
283+
return self.to_json_dict({
284+
'value': self.encode(self._value)
285+
}, **kwargs)
286+
287+
@classmethod
288+
def from_json(cls, json_value: JSONValueType, *args, **kwargs) -> Any:
289+
del args, kwargs
290+
assert isinstance(json_value, dict) and 'value' in json_value, json_value
291+
encoder = cls(json_value['value'], encoded=True)
292+
return encoder.value
293+
294+
249295
def registered_types() -> Iterable[Tuple[str, Type[JSONConvertible]]]:
250296
"""Returns an iterator of registered (serialization key, class) tuples."""
251297
return JSONConvertible.registered_types()
@@ -298,7 +344,7 @@ def to_json(value: Any, **kwargs) -> Any:
298344
converter = JSONConvertible.TYPE_CONVERTER(type(value)) # pylint: disable=not-callable
299345
if converter:
300346
return to_json(converter(value))
301-
raise ValueError(f'Cannot convert complex type {value} to JSON.')
347+
return _OpaqueObject(value).to_json(**kwargs)
302348

303349

304350
def from_json(json_value: JSONValueType) -> Any:
@@ -329,8 +375,6 @@ def from_json(json_value: JSONValueType) -> Any:
329375
return _function_from_json(json_value)
330376
elif type_name == 'method':
331377
return _method_from_json(json_value)
332-
# elif type_name == 'annotation':
333-
# return _annotation_from_json(json_value)
334378
else:
335379
cls = JSONConvertible.class_from_typename(type_name)
336380
if cls is None:
@@ -346,11 +390,6 @@ def from_json(json_value: JSONValueType) -> Any:
346390
#
347391

348392

349-
def _type_name(type_or_function: Union[Type[Any], types.FunctionType]) -> str:
350-
"""Returns the ID for a type or function."""
351-
return f'{type_or_function.__module__}.{type_or_function.__qualname__}'
352-
353-
354393
def _type_to_json(t: Type[Any]) -> Dict[str, str]:
355394
"""Converts a type to a JSON dict."""
356395
type_name = _type_name(t)

pyglove/core/object_utils/json_conversion_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Tests for pyglove.object_utils.json_conversion."""
1515

1616
import abc
17+
import datetime
1718
import typing
1819
import unittest
1920
from pyglove.core.object_utils import json_conversion
@@ -38,6 +39,15 @@ def static_method():
3839
def instance_method(self):
3940
return str(self)
4041

42+
def __init__(self, x):
43+
self.x = x
44+
45+
def __eq__(self, other):
46+
return isinstance(other, X) and self.x == other.x
47+
48+
def __ne__(self, other):
49+
return not self.__eq__(other)
50+
4151

4252
def bar():
4353
pass
@@ -256,6 +266,23 @@ def test_json_conversion_for_methods(self):
256266
ValueError, 'Cannot convert instance method .* to JSON.'):
257267
json_conversion.to_json(X.Y.Z().instance_method)
258268

269+
def test_json_conversion_for_opaque_objects(self):
270+
self.assert_conversion_equal(X(1))
271+
self.assert_conversion_equal(datetime.datetime.now())
272+
273+
class LocalX:
274+
pass
275+
276+
with self.assertRaisesRegex(
277+
ValueError, 'Cannot encode opaque object .* with pickle.'):
278+
json_conversion.to_json(LocalX())
279+
280+
json_dict = json_conversion.to_json(X(1))
281+
json_dict['value'] = 'abc'
282+
with self.assertRaisesRegex(
283+
ValueError, 'Cannot decode opaque object with pickle.'):
284+
json_conversion.from_json(json_dict)
285+
259286

260287
if __name__ == '__main__':
261288
unittest.main()

pyglove/core/symbolic/dict_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,7 +1854,7 @@ def __eq__(self, other):
18541854
])
18551855
sd = Dict(x=1, y=A(2.0), value_spec=spec)
18561856
with self.assertRaisesRegex(
1857-
ValueError, 'Cannot convert complex type .* to JSON.'):
1857+
ValueError, 'Cannot encode opaque object .* with pickle'):
18581858
sd.to_json_str()
18591859

18601860
pg_typing.register_converter(A, float, convert_fn=lambda x: x.value)
@@ -1905,7 +1905,7 @@ class A:
19051905
pass
19061906

19071907
with self.assertRaisesRegex(
1908-
ValueError, 'Cannot convert complex type .* to JSON.'):
1908+
ValueError, 'Cannot encode opaque object .* with pickle'):
19091909
base.to_json(Dict(x=A()))
19101910

19111911

pyglove/core/symbolic/list_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,7 +1593,7 @@ def __eq__(self, other):
15931593
]))
15941594
sl = List([dict(x=1, y=A(2.0))], value_spec=spec)
15951595
with self.assertRaisesRegex(
1596-
ValueError, 'Cannot convert complex type .* to JSON.'):
1596+
ValueError, 'Cannot encode opaque object .* with pickle.'):
15971597
sl.to_json_str()
15981598

15991599
pg_typing.register_converter(A, float, convert_fn=lambda x: x.value)
@@ -1646,7 +1646,7 @@ class A:
16461646
pass
16471647

16481648
with self.assertRaisesRegex(
1649-
ValueError, 'Cannot convert complex type .* to JSON.'):
1649+
ValueError, 'Cannot encode opaque object .* with pickle.'):
16501650
base.to_json(List([A()]))
16511651

16521652

pyglove/core/symbolic/object.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,9 @@ class Foo(pg.Object):
456456
Returns:
457457
A symbolic Object instance.
458458
"""
459-
return cls(allow_partial=allow_partial, root_path=root_path, **json_value)
459+
return cls(allow_partial=allow_partial, root_path=root_path, **{
460+
k: base.from_json(v) for k, v in json_value.items()
461+
})
460462

461463
@object_utils.explicit_method_override
462464
def __init__(

pyglove/core/symbolic/object_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,7 +2874,7 @@ def test_serialization_with_converter(self):
28742874

28752875
c = self._C(self._X(1), y=True)
28762876
with self.assertRaisesRegex(
2877-
ValueError, 'Cannot convert complex type .* to JSON.'):
2877+
ValueError, 'Cannot encode opaque object .* with pickle'):
28782878
c.to_json_str()
28792879

28802880
pg_typing.register_converter(self._X, int, convert_fn=lambda x: x.value)
@@ -2902,7 +2902,7 @@ class Z:
29022902
pass
29032903

29042904
with self.assertRaisesRegex(
2905-
ValueError, 'Cannot convert complex type .* to JSON.'):
2905+
ValueError, 'Cannot encode opaque object .* with pickle'):
29062906
base.to_json(self._A(w=Z(), y=True))
29072907

29082908
with self.assertRaisesRegex(

0 commit comments

Comments
 (0)