Repository: Azure/msrest-for-python

Issue:
Support building instance from dict with polymorphic object
Currently the SDKs can accept a dict instead of a model and transform it to the right model automatically. This is not available if there is at any level a polymorphic object.

This should be possible looking at `_subtype_map` to identify the right instance type

FYI @annatisch @vishrutshah

Discussion:

Patch:
diff --git a/msrest/serialization.py b/msrest/serialization.py
index ee81c21..6e2eb28 100644
--- a/msrest/serialization.py
+++ b/msrest/serialization.py
@@ -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):


Test patch:
diff --git a/test/unittest_serialization.py b/test/unittest_serialization.py
index 166a375..4e35926 100644
--- a/test/unittest_serialization.py
+++ b/test/unittest_serialization.py
@@ -562,51 +562,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):
 
@@ -614,12 +619,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": [ 
@@ -669,6 +676,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):
 
@@ -1100,48 +1141,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": [ 
             { 
@@ -1188,5 +1253,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()