diff --git a/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py b/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py index 7481abdf..3eab6165 100644 --- a/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py +++ b/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py @@ -19,6 +19,9 @@ from functools import partial +_JSON_PRIMITIVE_TYPES = {str, int, float, bool, type(None)} + + class DictionaryConverter: """ This is a base class used to convert BPMN specs, workflows, tasks, and (optonally) @@ -123,12 +126,24 @@ def restore(self, val, **kwargs): Returns: dict: the restored object for registered objects or the original for unregistered values """ - if isinstance(val, dict) and 'typename' in val: - from_dict = self.convert_from_dict.get(val.pop('typename')) - return from_dict(val, **kwargs) - elif isinstance(val, dict): + val_type = type(val) + if val_type in _JSON_PRIMITIVE_TYPES: + return val + + if isinstance(val, dict): + if 'typename' in val: + from_dict = self.convert_from_dict.get(val['typename']) + dct = dict(val) + del dct['typename'] + return from_dict(dct, **kwargs) return dict((k, self.restore(v, **kwargs)) for k, v in val.items()) + + if val_type is list: + return [self.restore(item, **kwargs) for item in val] + if val_type is tuple: + return tuple(self.restore(item, **kwargs) for item in val) + if val_type is set: + return {self.restore(item, **kwargs) for item in val} if isinstance(val, (list, tuple, set)): - return val.__class__([ self.restore(item, **kwargs) for item in val ]) - else: - return val + return val.__class__([self.restore(item, **kwargs) for item in val]) + return val diff --git a/tests/SpiffWorkflow/bpmn/serializer/DictionaryConverterTest.py b/tests/SpiffWorkflow/bpmn/serializer/DictionaryConverterTest.py new file mode 100644 index 00000000..4232c2c6 --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/serializer/DictionaryConverterTest.py @@ -0,0 +1,30 @@ +import unittest + +from SpiffWorkflow.bpmn.serializer.helpers.dictionary import DictionaryConverter + + +class DictionaryConverterTest(unittest.TestCase): + + def test_restore_typed_dict_does_not_mutate_input(self): + class Thing: + + def __init__(self, value): + self.value = value + + converter = DictionaryConverter() + converter.register( + Thing, + lambda thing: {'value': thing.value}, + lambda dct: Thing(dct['value']), + typename='Thing', + ) + data = { + 'typename': 'Thing', + 'value': 42, + } + + restored = converter.restore(data) + + self.assertIsInstance(restored, Thing) + self.assertEqual(42, restored.value) + self.assertEqual({'typename': 'Thing', 'value': 42}, data)