Merge pull request #12 from Azure/polymorphic_from_json
Fix #11 - Allow polymorphic serialization from JSON like objects
This commit is contained in:
Коммит
78d93295fd
|
@ -113,31 +113,38 @@ class Model(object):
|
|||
return base._subtype_map
|
||||
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
|
||||
def _classify(cls, response, objects):
|
||||
"""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:
|
||||
map = cls.__dict__.get('_subtype_map', {})
|
||||
for subtype_key in cls.__dict__.get('_subtype_map', {}).keys():
|
||||
subtype_value = None
|
||||
|
||||
for _type, _classes in map.items():
|
||||
classification = response.get(_type)
|
||||
try:
|
||||
return objects[_classes[classification]]
|
||||
except KeyError:
|
||||
pass
|
||||
rest_api_response_key = _decode_attribute_map_key(cls._attribute_map[subtype_key]['key'])
|
||||
subtype_value = response.pop(rest_api_response_key, None) or response.pop(subtype_key, None)
|
||||
if subtype_value:
|
||||
flatten_mapping_type = cls._flatten_subtype(subtype_key, objects)
|
||||
return objects[flatten_mapping_type[subtype_value]]
|
||||
return cls
|
||||
|
||||
for c in _classes:
|
||||
try:
|
||||
_cls = objects[_classes[c]]
|
||||
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.")
|
||||
def _decode_attribute_map_key(key):
|
||||
"""This decode a key in an _attribute_map to the actual key we want to look at
|
||||
inside the received data.
|
||||
|
||||
:param str key: A key string from the generated code
|
||||
"""
|
||||
return key.replace('\\.', '.')
|
||||
|
||||
def _convert_to_datatype(data, data_type, localtypes):
|
||||
if data is None:
|
||||
|
@ -157,6 +164,7 @@ def _convert_to_datatype(data, data_type, localtypes):
|
|||
elif issubclass(data_obj, Enum):
|
||||
return data
|
||||
elif not isinstance(data, data_obj):
|
||||
data_obj = data_obj._classify(data, localtypes)
|
||||
result = {
|
||||
key: _convert_to_datatype(
|
||||
data[key],
|
||||
|
@ -195,7 +203,7 @@ class Serializer(object):
|
|||
"unique": lambda x, y: len(x) != len(set(x)),
|
||||
"multiple": lambda x, y: x % y != 0
|
||||
}
|
||||
flattten = re.compile(r"(?<!\\)\.")
|
||||
flatten = re.compile(r"(?<!\\)\.")
|
||||
|
||||
def __init__(self, classes=None):
|
||||
self.serialize_type = {
|
||||
|
@ -241,14 +249,12 @@ class Serializer(object):
|
|||
|
||||
try:
|
||||
attributes = target_obj._attribute_map
|
||||
self._classify_data(target_obj, class_name, serialized)
|
||||
|
||||
for attr, map in attributes.items():
|
||||
attr_name = attr
|
||||
debug_name = "{}.{}".format(class_name, attr_name)
|
||||
try:
|
||||
keys = self.flattten.split(map['key'])
|
||||
keys = [k.replace('\\.', '.') for k in keys]
|
||||
keys = self.flatten.split(map['key'])
|
||||
keys = [_decode_attribute_map_key(k) for k in keys]
|
||||
attr_type = map['type']
|
||||
orig_attr = getattr(target_obj, attr)
|
||||
validation = target_obj._validation.get(attr_name, {})
|
||||
|
@ -278,18 +284,6 @@ class Serializer(object):
|
|||
else:
|
||||
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):
|
||||
"""Serialize data intended for a request body.
|
||||
|
||||
|
@ -752,9 +746,9 @@ class Deserializer(object):
|
|||
while '.' in key:
|
||||
dict_keys = self.flatten.split(key)
|
||||
if len(dict_keys) == 1:
|
||||
key = dict_keys[0].replace('\\.', '.')
|
||||
key = _decode_attribute_map_key(dict_keys[0])
|
||||
break
|
||||
working_key = dict_keys[0].replace('\\.', '.')
|
||||
working_key = _decode_attribute_map_key(dict_keys[0])
|
||||
working_data = working_data.get(working_key, data)
|
||||
key = '.'.join(dict_keys[1:])
|
||||
|
||||
|
@ -786,8 +780,8 @@ class Deserializer(object):
|
|||
|
||||
try:
|
||||
target = target._classify(data, self.dependencies)
|
||||
except (TypeError, AttributeError):
|
||||
pass # Target has no subclasses, so can't classify further.
|
||||
except AttributeError:
|
||||
pass # Target is not a Model, no classify
|
||||
return target, target.__class__.__name__
|
||||
|
||||
def _unpack_content(self, raw_data):
|
||||
|
|
|
@ -567,51 +567,56 @@ class TestRuntimeSerialized(unittest.TestCase):
|
|||
|
||||
_attribute_map = {
|
||||
"animals":{"key":"Animals", "type":"[Animal]"},
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.animals = None
|
||||
def __init__(self, animals=None):
|
||||
self.animals = animals
|
||||
|
||||
class Animal(Model):
|
||||
|
||||
_attribute_map = {
|
||||
"name":{"key":"Name", "type":"str"}
|
||||
}
|
||||
"name":{"key":"Name", "type":"str"},
|
||||
"d_type":{"key":"dType", "type":"str"}
|
||||
}
|
||||
|
||||
_subtype_map = {
|
||||
'dType': {"cat":"Cat", "dog":"Dog"}
|
||||
}
|
||||
'd_type': {"cat":"Cat", "dog":"Dog"}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.name = None
|
||||
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"}
|
||||
"likes_dog_food":{"key":"likesDogFood","type":"bool"},
|
||||
"d_type":{"key":"dType", "type":"str"}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.likes_dog_food = None
|
||||
super(Dog, self).__init__()
|
||||
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):
|
||||
|
||||
_attribute_map = {
|
||||
"name":{"key":"Name", "type":"str"},
|
||||
"likes_mice":{"key":"likesMice","type":"bool"},
|
||||
"dislikes":{"key":"dislikes","type":"Animal"}
|
||||
"dislikes":{"key":"dislikes","type":"Animal"},
|
||||
"d_type":{"key":"dType", "type":"str"}
|
||||
}
|
||||
|
||||
_subtype_map = {
|
||||
"dType":{"siamese":"Siamese"}
|
||||
"d_type":{"siamese":"Siamese"}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.likes_mice = None
|
||||
self.dislikes = None
|
||||
super(Cat, self).__init__()
|
||||
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):
|
||||
|
||||
|
@ -619,12 +624,14 @@ class TestRuntimeSerialized(unittest.TestCase):
|
|||
"name":{"key":"Name", "type":"str"},
|
||||
"likes_mice":{"key":"likesMice","type":"bool"},
|
||||
"dislikes":{"key":"dislikes","type":"Animal"},
|
||||
"color":{"key":"Color", "type":"str"}
|
||||
"color":{"key":"Color", "type":"str"},
|
||||
"d_type":{"key":"dType", "type":"str"}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.color = None
|
||||
super(Siamese, self).__init__()
|
||||
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 = {
|
||||
"Animals": [
|
||||
|
@ -674,6 +681,40 @@ class TestRuntimeSerialized(unittest.TestCase):
|
|||
serialized = self.s._serialize(zoo)
|
||||
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):
|
||||
|
||||
|
@ -1105,48 +1146,72 @@ class TestRuntimeDeserialized(unittest.TestCase):
|
|||
|
||||
_attribute_map = {
|
||||
"animals":{"key":"Animals", "type":"[Animal]"},
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, animals=None):
|
||||
self.animals = animals
|
||||
|
||||
class Animal(Model):
|
||||
|
||||
_attribute_map = {
|
||||
"name":{"key":"Name", "type":"str"}
|
||||
}
|
||||
|
||||
_test_attr = 123
|
||||
"name":{"key":"Name", "type":"str"},
|
||||
"d_type":{"key":"dType", "type":"str"}
|
||||
}
|
||||
|
||||
_subtype_map = {
|
||||
'dType': {"cat":"Cat", "dog":"Dog"}
|
||||
}
|
||||
'd_type': {"cat":"Cat", "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"}
|
||||
"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):
|
||||
|
||||
_attribute_map = {
|
||||
"name":{"key":"Name", "type":"str"},
|
||||
"likes_mice":{"key":"likesMice","type":"bool"},
|
||||
"dislikes":{"key":"dislikes","type":"Animal"}
|
||||
"dislikes":{"key":"dislikes","type":"Animal"},
|
||||
"d_type":{"key":"dType", "type":"str"}
|
||||
}
|
||||
|
||||
_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):
|
||||
|
||||
_attribute_map = {
|
||||
"name":{"key":"Name", "type":"str"},
|
||||
"likes_mice":{"key":"likesMice","type":"bool"},
|
||||
"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 = {
|
||||
"Animals": [
|
||||
{
|
||||
|
@ -1193,5 +1258,49 @@ class TestRuntimeDeserialized(unittest.TestCase):
|
|||
self.assertEqual(animals[2].color, message['Animals'][2]["Color"])
|
||||
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__':
|
||||
unittest.main()
|
||||
|
|
Загрузка…
Ссылка в новой задаче