Merge pull request #12 from Azure/polymorphic_from_json

Fix #11 - Allow polymorphic serialization from JSON like objects
This commit is contained in:
Laurent Mazuel 2017-02-10 12:29:09 -08:00 коммит произвёл GitHub
Родитель d6d55eec62 010d399383
Коммит 78d93295fd
2 изменённых файлов: 176 добавлений и 73 удалений

Просмотреть файл

@ -113,31 +113,38 @@ class Model(object):
return base._subtype_map return base._subtype_map
return {} return {}
@classmethod
def _flatten_subtype(cls, key, objects):
if not '_subtype_map' in cls.__dict__:
return {}
result = dict(cls._subtype_map[key])
for valuetype in cls._subtype_map[key].values():
result.update(objects[valuetype]._flatten_subtype(key, objects))
return result
@classmethod @classmethod
def _classify(cls, response, objects): def _classify(cls, response, objects):
"""Check the class _subtype_map for any child classes. """Check the class _subtype_map for any child classes.
We want to ignore any inheirited _subtype_maps. We want to ignore any inherited _subtype_maps.
Remove the polymorphic key from the initial data.
""" """
try: for subtype_key in cls.__dict__.get('_subtype_map', {}).keys():
map = cls.__dict__.get('_subtype_map', {}) subtype_value = None
for _type, _classes in map.items(): rest_api_response_key = _decode_attribute_map_key(cls._attribute_map[subtype_key]['key'])
classification = response.get(_type) subtype_value = response.pop(rest_api_response_key, None) or response.pop(subtype_key, None)
try: if subtype_value:
return objects[_classes[classification]] flatten_mapping_type = cls._flatten_subtype(subtype_key, objects)
except KeyError: return objects[flatten_mapping_type[subtype_value]]
pass return cls
for c in _classes: def _decode_attribute_map_key(key):
try: """This decode a key in an _attribute_map to the actual key we want to look at
_cls = objects[_classes[c]] inside the received data.
return _cls._classify(response, objects)
except (KeyError, TypeError):
continue
raise TypeError("Object cannot be classified futher.")
except AttributeError:
raise TypeError("Object cannot be classified futher.")
:param str key: A key string from the generated code
"""
return key.replace('\\.', '.')
def _convert_to_datatype(data, data_type, localtypes): def _convert_to_datatype(data, data_type, localtypes):
if data is None: if data is None:
@ -157,6 +164,7 @@ def _convert_to_datatype(data, data_type, localtypes):
elif issubclass(data_obj, Enum): elif issubclass(data_obj, Enum):
return data return data
elif not isinstance(data, data_obj): elif not isinstance(data, data_obj):
data_obj = data_obj._classify(data, localtypes)
result = { result = {
key: _convert_to_datatype( key: _convert_to_datatype(
data[key], data[key],
@ -195,7 +203,7 @@ class Serializer(object):
"unique": lambda x, y: len(x) != len(set(x)), "unique": lambda x, y: len(x) != len(set(x)),
"multiple": lambda x, y: x % y != 0 "multiple": lambda x, y: x % y != 0
} }
flattten = re.compile(r"(?<!\\)\.") flatten = re.compile(r"(?<!\\)\.")
def __init__(self, classes=None): def __init__(self, classes=None):
self.serialize_type = { self.serialize_type = {
@ -241,14 +249,12 @@ class Serializer(object):
try: try:
attributes = target_obj._attribute_map attributes = target_obj._attribute_map
self._classify_data(target_obj, class_name, serialized)
for attr, map in attributes.items(): for attr, map in attributes.items():
attr_name = attr attr_name = attr
debug_name = "{}.{}".format(class_name, attr_name) debug_name = "{}.{}".format(class_name, attr_name)
try: try:
keys = self.flattten.split(map['key']) keys = self.flatten.split(map['key'])
keys = [k.replace('\\.', '.') for k in keys] keys = [_decode_attribute_map_key(k) for k in keys]
attr_type = map['type'] attr_type = map['type']
orig_attr = getattr(target_obj, attr) orig_attr = getattr(target_obj, attr)
validation = target_obj._validation.get(attr_name, {}) validation = target_obj._validation.get(attr_name, {})
@ -278,18 +284,6 @@ class Serializer(object):
else: else:
return serialized return serialized
def _classify_data(self, target_obj, class_name, serialized):
"""Check whether this object is a child and therefor needs to be
classified in the message.
"""
try:
for _type, _classes in target_obj._get_subtype_map().items():
for ref, name in _classes.items():
if name == class_name:
serialized[_type] = ref
except AttributeError:
pass # TargetObj has no _subtype_map so we don't need to classify.
def body(self, data, data_type, **kwargs): def body(self, data, data_type, **kwargs):
"""Serialize data intended for a request body. """Serialize data intended for a request body.
@ -752,9 +746,9 @@ class Deserializer(object):
while '.' in key: while '.' in key:
dict_keys = self.flatten.split(key) dict_keys = self.flatten.split(key)
if len(dict_keys) == 1: if len(dict_keys) == 1:
key = dict_keys[0].replace('\\.', '.') key = _decode_attribute_map_key(dict_keys[0])
break break
working_key = dict_keys[0].replace('\\.', '.') working_key = _decode_attribute_map_key(dict_keys[0])
working_data = working_data.get(working_key, data) working_data = working_data.get(working_key, data)
key = '.'.join(dict_keys[1:]) key = '.'.join(dict_keys[1:])
@ -786,8 +780,8 @@ class Deserializer(object):
try: try:
target = target._classify(data, self.dependencies) target = target._classify(data, self.dependencies)
except (TypeError, AttributeError): except AttributeError:
pass # Target has no subclasses, so can't classify further. pass # Target is not a Model, no classify
return target, target.__class__.__name__ return target, target.__class__.__name__
def _unpack_content(self, raw_data): def _unpack_content(self, raw_data):

Просмотреть файл

@ -567,51 +567,56 @@ class TestRuntimeSerialized(unittest.TestCase):
_attribute_map = { _attribute_map = {
"animals":{"key":"Animals", "type":"[Animal]"}, "animals":{"key":"Animals", "type":"[Animal]"},
} }
def __init__(self): def __init__(self, animals=None):
self.animals = None self.animals = animals
class Animal(Model): class Animal(Model):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"} "name":{"key":"Name", "type":"str"},
} "d_type":{"key":"dType", "type":"str"}
}
_subtype_map = { _subtype_map = {
'dType': {"cat":"Cat", "dog":"Dog"} 'd_type': {"cat":"Cat", "dog":"Dog"}
} }
def __init__(self): def __init__(self, name=None):
self.name = None self.name = name
class Dog(Animal): class Dog(Animal):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"}, "name":{"key":"Name", "type":"str"},
"likes_dog_food":{"key":"likesDogFood","type":"bool"} "likes_dog_food":{"key":"likesDogFood","type":"bool"},
"d_type":{"key":"dType", "type":"str"}
} }
def __init__(self): def __init__(self, name=None, likes_dog_food=None):
self.likes_dog_food = None self.likes_dog_food = likes_dog_food
super(Dog, self).__init__() super(Dog, self).__init__(name)
self.d_type = 'dog'
class Cat(Animal): class Cat(Animal):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"}, "name":{"key":"Name", "type":"str"},
"likes_mice":{"key":"likesMice","type":"bool"}, "likes_mice":{"key":"likesMice","type":"bool"},
"dislikes":{"key":"dislikes","type":"Animal"} "dislikes":{"key":"dislikes","type":"Animal"},
"d_type":{"key":"dType", "type":"str"}
} }
_subtype_map = { _subtype_map = {
"dType":{"siamese":"Siamese"} "d_type":{"siamese":"Siamese"}
} }
def __init__(self): def __init__(self, name=None, likes_mice=None, dislikes = None):
self.likes_mice = None self.likes_mice = likes_mice
self.dislikes = None self.dislikes = dislikes
super(Cat, self).__init__() super(Cat, self).__init__(name)
self.d_type = 'cat'
class Siamese(Cat): class Siamese(Cat):
@ -619,12 +624,14 @@ class TestRuntimeSerialized(unittest.TestCase):
"name":{"key":"Name", "type":"str"}, "name":{"key":"Name", "type":"str"},
"likes_mice":{"key":"likesMice","type":"bool"}, "likes_mice":{"key":"likesMice","type":"bool"},
"dislikes":{"key":"dislikes","type":"Animal"}, "dislikes":{"key":"dislikes","type":"Animal"},
"color":{"key":"Color", "type":"str"} "color":{"key":"Color", "type":"str"},
"d_type":{"key":"dType", "type":"str"}
} }
def __init__(self): def __init__(self, name=None, likes_mice=None, dislikes = None, color=None):
self.color = None self.color = color
super(Siamese, self).__init__() super(Siamese, self).__init__(name, likes_mice, dislikes)
self.d_type = 'siamese'
message = { message = {
"Animals": [ "Animals": [
@ -674,6 +681,40 @@ class TestRuntimeSerialized(unittest.TestCase):
serialized = self.s._serialize(zoo) serialized = self.s._serialize(zoo)
self.assertEqual(serialized, message) self.assertEqual(serialized, message)
old_dependencies = self.s.dependencies
self.s.dependencies = {
'Zoo': Zoo,
'Animal': Animal,
'Dog': Dog,
'Cat': Cat,
'Siamese': Siamese
}
serialized = self.s.body({
"animals": [{
"dType": "dog",
"likes_dog_food": True,
"name": "Fido"
},{
"dType": "cat",
"likes_mice": False,
"dislikes": {
"dType": "dog",
"likes_dog_food": True,
"name": "Angry"
},
"name": "Felix"
},{
"dType": "siamese",
"color": "grey",
"likes_mice": True,
"name": "Finch"
}]
}, "Zoo")
self.assertEqual(serialized, message)
self.s.dependencies = old_dependencies
class TestRuntimeDeserialized(unittest.TestCase): class TestRuntimeDeserialized(unittest.TestCase):
@ -1105,48 +1146,72 @@ class TestRuntimeDeserialized(unittest.TestCase):
_attribute_map = { _attribute_map = {
"animals":{"key":"Animals", "type":"[Animal]"}, "animals":{"key":"Animals", "type":"[Animal]"},
} }
def __init__(self, animals=None):
self.animals = animals
class Animal(Model): class Animal(Model):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"} "name":{"key":"Name", "type":"str"},
} "d_type":{"key":"dType", "type":"str"}
}
_test_attr = 123
_subtype_map = { _subtype_map = {
'dType': {"cat":"Cat", "dog":"Dog"} 'd_type': {"cat":"Cat", "dog":"Dog"}
} }
def __init__(self, name=None):
self.name = name
class Dog(Animal): class Dog(Animal):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"}, "name":{"key":"Name", "type":"str"},
"likes_dog_food":{"key":"likesDogFood","type":"bool"} "likes_dog_food":{"key":"likesDogFood","type":"bool"},
"d_type":{"key":"dType", "type":"str"}
} }
def __init__(self, name=None, likes_dog_food=None):
self.likes_dog_food = likes_dog_food
super(Dog, self).__init__(name)
self.d_type = 'dog'
class Cat(Animal): class Cat(Animal):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"}, "name":{"key":"Name", "type":"str"},
"likes_mice":{"key":"likesMice","type":"bool"}, "likes_mice":{"key":"likesMice","type":"bool"},
"dislikes":{"key":"dislikes","type":"Animal"} "dislikes":{"key":"dislikes","type":"Animal"},
"d_type":{"key":"dType", "type":"str"}
} }
_subtype_map = { _subtype_map = {
"dType":{"siamese":"Siamese"} "d_type":{"siamese":"Siamese"}
} }
def __init__(self, name=None, likes_mice=None, dislikes = None):
self.likes_mice = likes_mice
self.dislikes = dislikes
super(Cat, self).__init__(name)
self.d_type = 'cat'
class Siamese(Cat): class Siamese(Cat):
_attribute_map = { _attribute_map = {
"name":{"key":"Name", "type":"str"}, "name":{"key":"Name", "type":"str"},
"likes_mice":{"key":"likesMice","type":"bool"}, "likes_mice":{"key":"likesMice","type":"bool"},
"dislikes":{"key":"dislikes","type":"Animal"}, "dislikes":{"key":"dislikes","type":"Animal"},
"color":{"key":"Color", "type":"str"} "color":{"key":"Color", "type":"str"},
"d_type":{"key":"dType", "type":"str"}
} }
def __init__(self, name=None, likes_mice=None, dislikes = None, color=None):
self.color = color
super(Siamese, self).__init__(name, likes_mice, dislikes)
self.d_type = 'siamese'
message = { message = {
"Animals": [ "Animals": [
{ {
@ -1193,5 +1258,49 @@ class TestRuntimeDeserialized(unittest.TestCase):
self.assertEqual(animals[2].color, message['Animals'][2]["Color"]) self.assertEqual(animals[2].color, message['Animals'][2]["Color"])
self.assertTrue(animals[2].likes_mice) self.assertTrue(animals[2].likes_mice)
def test_polymorphic_deserialization_with_escape(self):
class Animal(Model):
_attribute_map = {
"name":{"key":"Name", "type":"str"},
"d_type":{"key":"odata\\.type", "type":"str"}
}
_subtype_map = {
'd_type': {"dog":"Dog"}
}
def __init__(self, name=None):
self.name = name
class Dog(Animal):
_attribute_map = {
"name":{"key":"Name", "type":"str"},
"likes_dog_food":{"key":"likesDogFood","type":"bool"},
"d_type":{"key":"odata\\.type", "type":"str"}
}
def __init__(self, name=None, likes_dog_food=None):
self.likes_dog_food = likes_dog_food
super(Dog, self).__init__(name)
self.d_type = 'dog'
message = {
"odata.type": "dog",
"likesDogFood": True,
"Name": "Fido"
}
self.d.dependencies = {
'Animal':Animal, 'Dog':Dog}
animal = self.d('Animal', message)
self.assertIsInstance(animal, Dog)
self.assertTrue(animal.likes_dog_food)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()