(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:
Dmitry S
2024-09-03 01:58:48 -04:00
parent 994c8d3faa
commit 08b91c4cb7
3 changed files with 482 additions and 3 deletions

View File

@@ -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):

View 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()