mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Fix serialization of values derived from primitive types, like IntEnum.
Summary: There is a range of types that -- when returned from formulas -- used to cause bad errors (that looked like a data engine crash and were reported as "Memory Error") because they looked like primitive types but were not marshallable. For example, IntEnum. We now encode such values as the primitive type they are based on. Test Plan: - Added a unittest that encode_object() now handles problematic values. - Added a browser test case that problematic values are no longer causing errors. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4336
This commit is contained in:
@@ -167,15 +167,26 @@ def encode_object(value):
|
||||
Produces a Grist-encoded version of the value, e.g. turning a Date into ['d', timestamp].
|
||||
Returns ['U', repr(value)] if it fails to encode otherwise.
|
||||
"""
|
||||
# pylint: disable=unidiomatic-typecheck
|
||||
try:
|
||||
if isinstance(value, (six.text_type, float, bool)) or value is None:
|
||||
# A primitive type can be returned directly.
|
||||
if type(value) in (six.text_type, float, bool) or value is None:
|
||||
return value
|
||||
# Other instances of these types must be derived; cast these to the primitive type to ensure
|
||||
# they are marshallable.
|
||||
elif isinstance(value, six.text_type):
|
||||
return six.text_type(value)
|
||||
elif isinstance(value, float):
|
||||
return float(value)
|
||||
elif isinstance(value, bool):
|
||||
return bool(value)
|
||||
elif isinstance(value, six.binary_type):
|
||||
return value.decode('utf8')
|
||||
elif isinstance(value, six.integer_types):
|
||||
if not is_int_short(value):
|
||||
raise UnmarshallableError("Integer too large")
|
||||
return value
|
||||
return ['U', str(value)]
|
||||
# Cast to a primitive type to ensure it's marshallable (e.g. enum.IntEnum would not be).
|
||||
return int(value)
|
||||
elif isinstance(value, AltText):
|
||||
return six.text_type(value)
|
||||
elif isinstance(value, records.Record):
|
||||
|
||||
72
sandbox/grist/test_objtypes.py
Normal file
72
sandbox/grist/test_objtypes.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import datetime
|
||||
import enum
|
||||
import marshal
|
||||
import unittest
|
||||
|
||||
import objtypes
|
||||
|
||||
class TestObjTypes(unittest.TestCase):
|
||||
class Int(int):
|
||||
pass
|
||||
class Float(float):
|
||||
pass
|
||||
class Text(str):
|
||||
pass
|
||||
class MyEnum(enum.IntEnum):
|
||||
ONE = 1
|
||||
class FussyFloat(float):
|
||||
def __float__(self):
|
||||
raise TypeError("Cannot cast FussyFloat to float")
|
||||
|
||||
|
||||
# (value, expected encoded value, expected decoded value)
|
||||
values = [
|
||||
(17, 17),
|
||||
(-17, -17),
|
||||
(0, 0),
|
||||
# The following is an unmarshallable value.
|
||||
(12345678901234567890, ['U', '12345678901234567890']),
|
||||
(0.0, 0.0),
|
||||
(1e-20, 1e-20),
|
||||
(1e20, 1e20),
|
||||
(1e40, 1e40),
|
||||
(float('infinity'), float('infinity')),
|
||||
(True, True),
|
||||
(Int(5), 5),
|
||||
(MyEnum.ONE, 1),
|
||||
(Float(3.3), 3.3),
|
||||
(Text("Hello"), u"Hello"),
|
||||
(datetime.date(2024, 9, 2), ['d', 1725235200.0]),
|
||||
(datetime.datetime(2024, 9, 2, 3, 8, 21), ['D', 1725246501, 'UTC']),
|
||||
# This is also unmarshallable.
|
||||
(FussyFloat(17.0), ['U', '17.0']),
|
||||
# Various other values are unmarshallable too.
|
||||
(len, ['U', '<built-in function len>']),
|
||||
# List, and list with an unmarshallable value.
|
||||
([Float(6), "", MyEnum.ONE], ['L', 6, "", 1]),
|
||||
([Text("foo"), FussyFloat(-0.5)], ['L', "foo", ['U', '-0.5']]),
|
||||
]
|
||||
|
||||
def test_encode_object(self):
|
||||
for (value, expected_encoded) in self.values:
|
||||
encoded = objtypes.encode_object(value)
|
||||
|
||||
# Check that encoding is as expected.
|
||||
self.assertStrictEqual(encoded, expected_encoded, 'encoding of %r' % value)
|
||||
|
||||
# Check it can be round-tripped through marshalling.
|
||||
marshaled = marshal.dumps(encoded)
|
||||
self.assertStrictEqual(marshal.loads(marshaled), encoded, 'de-marshalling of %r' % value)
|
||||
|
||||
# Check that the decoded value, though it may not be identical, encodes identically.
|
||||
decoded = objtypes.decode_object(encoded)
|
||||
re_encoded = objtypes.encode_object(decoded)
|
||||
self.assertStrictEqual(re_encoded, encoded, 're-encoding of %r' % value)
|
||||
|
||||
def assertStrictEqual(self, a, b, msg=None):
|
||||
self.assertEqual(a, b, '%s: %r != %r' % (msg, a, b))
|
||||
self.assertEqual(type(a), type(b), '%s: %r != %r' % (msg, type(a), type(b)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user