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 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()
|
||||||
|
|
Загрузка…
Ссылка в новой задаче