Additional properties support (#71)

* Additional properties support

* Improve AdditionalProperties for Python

* Do not set add_prop if nothing to set (backward compat)

* Disambiguate add_prop if defined as a regular property

* Make addProp robust to explicit property declaration
This commit is contained in:
Laurent Mazuel 2017-12-13 13:27:45 -08:00 коммит произвёл GitHub
Родитель 8a541abcb8
Коммит ae963d0b2e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 281 добавлений и 3 удалений

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

@ -136,6 +136,7 @@ class Model(object):
"""Allow attribute setting via kwargs on initialization."""
for k in kwargs:
setattr(self, k, kwargs[k])
self.additional_properties = {}
def __eq__(self, other):
"""Compare objects by comparing all attributes."""
@ -150,6 +151,10 @@ class Model(object):
def __str__(self):
return str(self.__dict__)
@classmethod
def enable_additional_properties_sending(cls):
cls._attribute_map['additional_properties'] = {'key': '', 'type': '{object}'}
def validate(self):
"""Validate this model recursively and return a list of ValidationError.
@ -393,6 +398,9 @@ class Serializer(object):
attributes = target_obj._attribute_map
for attr, attr_desc in attributes.items():
attr_name = attr
if attr_name == "additional_properties" and attr_desc["key"] == '' and target_obj.additional_properties:
serialized.update(target_obj.additional_properties)
continue
if not keep_readonly and target_obj._validation.get(attr_name, {}).get('readonly', False):
continue
try:
@ -990,7 +998,9 @@ class Deserializer(object):
attributes = response._attribute_map
d_attrs = {}
for attr, attr_desc in attributes.items():
# Check empty string. If it's not empty, someone has a real "additionalProperties"...
if attr == "additional_properties" and attr_desc["key"] == '':
continue
raw_value = None
for key_extractor in self.key_extractors:
found_value = key_extractor(attr, attr_desc, data)
@ -1005,7 +1015,17 @@ class Deserializer(object):
msg = "Unable to deserialize to object: " + class_name
raise_with_traceback(DeserializationError, msg, err)
else:
return self._instantiate_model(response, d_attrs)
additional_properties = self._build_additional_properties(response._attribute_map, data)
return self._instantiate_model(response, d_attrs, additional_properties)
def _build_additional_properties(self, attribute_map, data):
if "additional_properties" in attribute_map and attribute_map.get("additional_properties", {}).get("key") != '':
# Check empty string. If it's not empty, someone has a real "additionalProperties"
return None
known_json_keys = {desc['key'] for desc in attribute_map.values() if desc['key'] != ''}
present_json_keys = set(data.keys())
missing_keys = present_json_keys - known_json_keys
return {key: data[key] for key in missing_keys}
def _classify_target(self, target, data):
"""Check to see whether the deserialization target object can
@ -1082,7 +1102,7 @@ class Deserializer(object):
raise DeserializationError("Do not support XML right now")
return data
def _instantiate_model(self, response, attrs):
def _instantiate_model(self, response, attrs, additional_properties=None):
"""Instantiate a response model passing in deserialized args.
:param response: The response model class.
@ -1100,6 +1120,8 @@ class Deserializer(object):
response_obj = response(**kwargs)
for attr in readonly:
setattr(response_obj, attr, attrs.get(attr))
if additional_properties:
response_obj.additional_properties = additional_properties
return response_obj
except TypeError as err:
msg = "Unable to deserialize {} into model {}. ".format(

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

@ -1001,6 +1001,167 @@ class TestRuntimeSerialized(unittest.TestCase):
self.s.dependencies = old_dependencies
def test_additional_properties_no_send(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"}
}
def __init__(self, name=None):
self.name = name
o = AdditionalTest(
name='test'
)
o.additional_properties={
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
expected_message = {
"Name": "test"
}
s = Serializer({'AdditionalTest': AdditionalTest})
serialized = s.body(o, 'AdditionalTest')
self.assertEqual(serialized, expected_message)
def test_additional_properties_manual(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"}
}
def __init__(self, name=None):
self.name = name
AdditionalTest.enable_additional_properties_sending()
o = AdditionalTest(
name='test'
)
o.additional_properties={
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
expected_message = {
"Name": "test",
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
s = Serializer({'AdditionalTest': AdditionalTest})
serialized = s.body(o, 'AdditionalTest')
self.assertEqual(serialized, expected_message)
def test_additional_properties(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"},
'additional_properties': {'key': '', 'type': '{object}'}
}
def __init__(self, name=None, additional_properties=None):
self.name = name
self.additional_properties = additional_properties
o = AdditionalTest(
name='test',
additional_properties={
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
)
expected_message = {
"Name": "test",
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
s = Serializer({'AdditionalTest': AdditionalTest})
serialized = s.body(o, 'AdditionalTest')
self.assertEqual(serialized, expected_message)
def test_additional_properties_declared(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"},
'additional_properties': {'key': 'AddProp', 'type': '{object}'}
}
def __init__(self, name=None, additional_properties=None):
self.name = name
self.additional_properties = additional_properties
o = AdditionalTest(
name='test',
additional_properties={
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
)
expected_message = {
"Name": "test",
"AddProp": {
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
}
s = Serializer({'AdditionalTest': AdditionalTest})
serialized = s.body(o, 'AdditionalTest')
self.assertEqual(serialized, expected_message)
# Make it declared as a property AND readonly
AdditionalTest._validation = {
'additional_properties': {'readonly': True}
}
expected_message = {
"Name": "test"
}
s = Serializer({'AdditionalTest': AdditionalTest})
serialized = s.body(o, 'AdditionalTest')
self.assertEqual(serialized, expected_message)
class TestRuntimeDeserialized(unittest.TestCase):
class TestObj(Model):
@ -1821,6 +1982,101 @@ class TestRuntimeDeserialized(unittest.TestCase):
self.assertIsInstance(animal, Dog)
self.assertTrue(animal.likes_dog_food)
def test_additional_properties(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"},
'additional_properties': {'key': '', 'type': '{object}'}
}
def __init__(self, name=None, additional_properties=None):
self.name = name
self.additional_properties = additional_properties
message = {
"Name": "test",
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
d = Deserializer({'AdditionalTest': AdditionalTest})
m = d('AdditionalTest', message)
self.assertEquals(m.name, "test")
self.assertEquals(m.additional_properties['PropInt'], 2)
self.assertEquals(m.additional_properties['PropStr'], "AdditionalProperty")
self.assertEquals(m.additional_properties['PropArray'], [1,2,3])
self.assertEquals(m.additional_properties['PropDict'], {"a": "b"})
def test_additional_properties_declared(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"},
'additional_properties': {'key': 'AddProp', 'type': '{object}'}
}
def __init__(self, name=None, additional_properties=None):
self.name = name
self.additional_properties = additional_properties
message = {
"Name": "test",
"AddProp": {
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
}
d = Deserializer({'AdditionalTest': AdditionalTest})
m = d('AdditionalTest', message)
self.assertEquals(m.name, "test")
self.assertEquals(m.additional_properties['PropInt'], 2)
self.assertEquals(m.additional_properties['PropStr'], "AdditionalProperty")
self.assertEquals(m.additional_properties['PropArray'], [1,2,3])
self.assertEquals(m.additional_properties['PropDict'], {"a": "b"})
def test_additional_properties_not_configured(self):
class AdditionalTest(Model):
_attribute_map = {
"name": {"key":"Name", "type":"str"}
}
def __init__(self, name=None):
self.name = name
message = {
"Name": "test",
"PropInt": 2,
"PropStr": "AdditionalProperty",
"PropArray": [1,2,3],
"PropDict": {"a": "b"}
}
d = Deserializer({'AdditionalTest': AdditionalTest})
m = d('AdditionalTest', message)
self.assertEquals(m.name, "test")
self.assertEquals(m.additional_properties['PropInt'], 2)
self.assertEquals(m.additional_properties['PropStr'], "AdditionalProperty")
self.assertEquals(m.additional_properties['PropArray'], [1,2,3])
self.assertEquals(m.additional_properties['PropDict'], {"a": "b"})
class TestModelInstanceEquality(unittest.TestCase):
def test_model_instance_equality(self):