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:
Родитель
8a541abcb8
Коммит
ae963d0b2e
|
@ -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):
|
||||
|
|
Загрузка…
Ссылка в новой задаче