Skip to content
Snippets Groups Projects
Commit a39c2626 authored by Nicolas Dandrimont's avatar Nicolas Dandrimont :thinking:
Browse files

New upstream version 0.6.10

parent cc0edfde
No related branches found
No related tags found
No related merge requests found
......@@ -20,7 +20,22 @@ To install:
Release History
---------------
2019-07-24 Versopm 0.6.9
2019-09-04 Version 0.6.10
+++++++++++++++++++++++++
**Features**
- XML mode now supports OpenAPI additional properties # 174
**Bugfixes**
- Accept "is_xml" kwargs to force XML serialization #178
- Disable XML deserialization if received element is not an ElementTree #178
- A "null" enum deserialize as None, and not "None" anymore #173
- Fix some UTF8 encoding issue in Python 2.7 and XML mode #172
2019-07-24 Version 0.6.9
++++++++++++++++++++++++
**Features**
......
......@@ -145,7 +145,7 @@ class LROPoller(object):
"""Start the long running operation.
On completion, runs any callbacks.
:param callable update_cmd: The API reuqest to check the status of
:param callable update_cmd: The API request to check the status of
the operation.
"""
try:
......
......@@ -51,8 +51,10 @@ from .exceptions import (
try:
basestring # type: ignore
unicode_str = unicode # type: ignore
except NameError:
basestring = str # type: ignore
unicode_str = str # type: ignore
_LOGGER = logging.getLogger(__name__)
......@@ -150,8 +152,7 @@ class Model(object):
_validation = {} # type: Dict[str, Dict[str, Any]]
def __init__(self, **kwargs):
if not self.is_xml_model():
self.additional_properties = {}
self.additional_properties = {}
for k in kwargs:
if k not in self._attribute_map:
_LOGGER.warning("%s is not a known attribute of class %s and will be ignored", k, self.__class__)
......@@ -175,8 +176,6 @@ class Model(object):
@classmethod
def enable_additional_properties_sending(cls):
if cls.is_xml_model():
raise ValueError("XML model are not compatible with additionalProperties")
cls._attribute_map['additional_properties'] = {'key': '', 'type': '{object}'}
@classmethod
......@@ -422,15 +421,6 @@ class Serializer(object):
self.key_transformer = full_restapi_key_transformer
self.client_side_validation = True
@staticmethod
def _create_serialized_base(target_obj):
"""Create the base for this serialization.
- For JSON, it's a simple empty dict
- For XML, it's an element (with namespace if needed)
"""
return {} if not target_obj.is_xml_model() else target_obj._create_xml_node()
def _serialize(self, target_obj, data_type=None, **kwargs):
"""Serialize data into a string according to type.
......@@ -463,7 +453,9 @@ class Serializer(object):
except KeyError:
is_xml_model_serialization = kwargs.setdefault("is_xml", target_obj.is_xml_model())
serialized = self._create_serialized_base(target_obj) # Could be JSON or XML
serialized = {}
if is_xml_model_serialization:
serialized = target_obj._create_xml_node()
try:
attributes = target_obj._attribute_map
for attr, attr_desc in attributes.items():
......@@ -478,7 +470,7 @@ class Serializer(object):
try:
### Extract sub-data to serialize from model ###
orig_attr = getattr(target_obj, attr)
if target_obj.is_xml_model():
if is_xml_model_serialization:
pass # Don't provide "transformer" for XML for now. Keep "orig_attr"
else: # JSON
keys, orig_attr = key_transformer(attr, attr_desc.copy(), orig_attr)
......@@ -513,7 +505,7 @@ class Serializer(object):
xml_desc.get('prefix', None),
xml_desc.get('ns', None)
)
local_node.text = str(new_attr)
local_node.text = unicode_str(new_attr)
serialized.append(local_node)
else: # JSON
for k in reversed(keys):
......@@ -552,13 +544,20 @@ class Serializer(object):
# Just in case this is a dict
internal_data_type = data_type.strip('[]{}')
internal_data_type = self.dependencies.get(internal_data_type, None)
try:
is_xml_model_serialization = kwargs["is_xml"]
except KeyError:
if internal_data_type and issubclass(internal_data_type, Model):
is_xml_model_serialization = kwargs.setdefault("is_xml", internal_data_type.is_xml_model())
else:
is_xml_model_serialization = False
if internal_data_type and not isinstance(internal_data_type, Enum):
try:
deserializer = Deserializer(self.dependencies)
# Since it's on serialization, it's almost sure that format is not JSON REST
# We're not able to deal with additional properties for now.
deserializer.additional_properties_detection = False
if issubclass(internal_data_type, Model) and internal_data_type.is_xml_model():
if is_xml_model_serialization:
deserializer.key_extractors = [
attribute_key_case_insensitive_extractor,
]
......@@ -726,8 +725,8 @@ class Serializer(object):
Serializes objects to str, int, float or bool.
Possible kwargs:
- is_xml bool : If set, adapt basic serializers without the need for basic_types_serializers
- basic_types_serializers dict[str, callable] : If set, use the callable as serializer
- is_xml bool : If set, use xml_basic_types_serializers
:param data: Object to be serialized.
:param str data_type: Type of object in the iterable.
......@@ -1108,6 +1107,9 @@ def xml_key_extractor(attr, attr_desc, data):
if 'xml' not in attr_desc:
return None
if isinstance(data, dict):
return None
xml_desc = attr_desc['xml']
xml_name = xml_desc['name']
xml_ns = xml_desc.get('ns', None)
......@@ -1309,10 +1311,13 @@ class Deserializer(object):
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 = {_decode_attribute_map_key(_FLATTEN.split(desc['key'])[0])
for desc in attribute_map.values() if desc['key'] != ''}
present_json_keys = set(data.keys())
missing_keys = present_json_keys - known_json_keys
if isinstance(data, ET.Element):
data = {el.tag: el.text for el in data}
known_keys = {_decode_attribute_map_key(_FLATTEN.split(desc['key'])[0])
for desc in attribute_map.values() if desc['key'] != ''}
present_keys = set(data.keys())
missing_keys = present_keys - known_keys
return {key: data[key] for key in missing_keys}
def _classify_target(self, target, data):
......@@ -1605,12 +1610,15 @@ class Deserializer(object):
def deserialize_enum(data, enum_obj):
"""Deserialize string into enum object.
:param str data: response string to be deserialized.
If the string is not a valid enum value it will be returned as-is
and a warning will be logged.
:param str data: Response string to be deserialized. If this value is
None or invalid it will be returned as-is.
:param Enum enum_obj: Enum object to deserialize to.
:rtype: Enum
:raises: DeserializationError if string is not valid enum value.
"""
if isinstance(data, enum_obj):
if isinstance(data, enum_obj) or data is None:
return data
if isinstance(data, Enum):
data = data.value
......
......@@ -25,4 +25,4 @@
# --------------------------------------------------------------------------
#: version of this package. Use msrest.__version__ instead
msrest_version = "0.6.9"
msrest_version = "0.6.10"
......@@ -28,7 +28,7 @@ from setuptools import setup, find_packages
setup(
name='msrest',
version='0.6.9',
version='0.6.10',
author='Microsoft Corporation',
packages=find_packages(exclude=["tests", "tests.*"]),
url=("https://github.com/Azure/msrest-for-python"),
......
......@@ -163,6 +163,32 @@ class TestModelDeserialization(unittest.TestCase):
self.assertEqual(len(cm.output), 1)
self.assertIn("not a known attribute", cm.output[0])
@unittest.skipIf(sys.version_info < (3,4), "assertLogs not supported before 3.4")
def test_empty_enum_logs(self):
class StatusType(str, Enum):
success = "success"
failed = "failed"
d = Deserializer({"StatusType": StatusType})
with self.assertRaises(AssertionError):
with self.assertLogs('msrest.serialization', level='WARNING') as cm:
result = d(StatusType, "failed")
self.assertEqual(len(cm.output), 0)
self.assertEqual(result, StatusType.failed)
with self.assertRaises(AssertionError):
with self.assertLogs('msrest.serialization', level='WARNING') as cm:
result = d(StatusType, None)
self.assertEqual(len(cm.output), 0)
self.assertEqual(result, None)
with self.assertLogs('msrest.serialization', level='WARNING') as cm:
result = d(StatusType, "aborted")
self.assertEqual(result, 'aborted')
self.assertEqual(len(cm.output), 1)
self.assertTrue("Deserializer is not able to find aborted as valid enum" in cm.output[0])
def test_response(self):
data = {
......@@ -1275,6 +1301,27 @@ class TestRuntimeSerialized(unittest.TestCase):
'data': {'id': long_type(1)}
}
def test_json_with_xml_map(self):
basic_json = {'age': 37, 'country': 'france'}
class XmlModel(Model):
_attribute_map = {
'age': {'key': 'age', 'type': 'int', 'xml':{'name': 'Age'}},
'country': {'key': 'country', 'type': 'str', 'xml':{'name': 'country', 'attr': True}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
age=37,
country="france",
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(mymodel, 'XmlModel', is_xml=False)
assert rawxml==basic_json
class TestRuntimeDeserialized(unittest.TestCase):
......
......@@ -422,6 +422,35 @@ class TestXmlDeserialization:
assert [apple.name for apple in result.good_apples] == ["granny", "fuji"]
def test_basic_additional_properties(self):
"""Test an ultra basic XML."""
basic_xml = """<?xml version="1.0"?>
<Metadata>
<number>1</number>
<name>bob</name>
</Metadata>"""
class XmlModel(Model):
_attribute_map = {
'additional_properties': {'key': '', 'type': '{str}', 'xml': {'name': 'additional_properties'}},
'encrypted': {'key': 'Encrypted', 'type': 'str', 'xml': {'name': 'Encrypted', 'attr': True}},
}
_xml_map = {
'name': 'Metadata'
}
def __init__(self, **kwargs):
super(XmlModel, self).__init__(**kwargs)
self.additional_properties = kwargs.get('additional_properties', None)
self.encrypted = kwargs.get('encrypted', None)
s = Deserializer({"XmlModel": XmlModel})
result = s(XmlModel, basic_xml, "application/xml")
assert result.additional_properties == {'name': 'bob', 'number': '1'}
assert result.encrypted is None
def test_basic_namespace(self):
"""Test an ultra basic XML."""
basic_xml = """<?xml version="1.0"?>
......@@ -493,6 +522,28 @@ class TestXmlSerialization:
assert_xml_equals(rawxml, basic_xml)
def test_nested_unicode(self):
class XmlModel(Model):
_attribute_map = {
'message_text': {'key': 'MessageText', 'type': 'str', 'xml': {'name': 'MessageText'}},
}
_xml_map = {
'name': 'Message'
}
mymodel_no_unicode = XmlModel(message_text=u'message1')
s = Serializer({"XmlModel": XmlModel})
body = s.body(mymodel_no_unicode, 'XmlModel')
xml_content = ET.tostring(body, encoding="utf8")
assert xml_content == b"<?xml version='1.0' encoding='utf8'?>\n<Message><MessageText>message1</MessageText></Message>"
mymodel_with_unicode = XmlModel(message_text=u'message1㚈')
s = Serializer({"XmlModel": XmlModel})
body = s.body(mymodel_with_unicode, 'XmlModel')
xml_content = ET.tostring(body, encoding="utf8")
assert xml_content == b"<?xml version='1.0' encoding='utf8'?>\n<Message><MessageText>message1\xe3\x9a\x88</MessageText></Message>"
@pytest.mark.skipif(sys.version_info < (3,6),
reason="Dict ordering not guaranted before 3.6, makes this complicated to test.")
......@@ -877,3 +928,435 @@ class TestXmlSerialization:
assert_xml_equals(rawxml, basic_xml)
def test_basic_is_xml(self):
"""Test an ultra basic XML."""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<Data country="france">
<Age>37</Age>
</Data>""")
class XmlModel(Model):
_attribute_map = {
'age': {'key': 'age', 'type': 'int', 'xml':{'name': 'Age'}},
'country': {'key': 'country', 'type': 'str', 'xml':{'name': 'country', 'attr': True}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
age=37,
country="france",
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(mymodel, 'XmlModel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_basic_unicode_is_xml(self):
"""Test a XML with unicode."""
basic_xml = ET.fromstring(u"""<?xml version="1.0" encoding="utf-8"?>
<Data language="français"/>""".encode("utf-8"))
class XmlModel(Model):
_attribute_map = {
'language': {'key': 'language', 'type': 'str', 'xml':{'name': 'language', 'attr': True}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
language=u"français"
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(mymodel, 'XmlModel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
@pytest.mark.skipif(sys.version_info < (3,6),
reason="Dict ordering not guaranted before 3.6, makes this complicated to test.")
def test_add_prop_is_xml(self):
"""Test addProp as a dict.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<Data>
<Metadata>
<Key1>value1</Key1>
<Key2>value2</Key2>
</Metadata>
</Data>""")
class XmlModel(Model):
_attribute_map = {
'metadata': {'key': 'Metadata', 'type': '{str}', 'xml': {'name': 'Metadata'}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
metadata={
'Key1': 'value1',
'Key2': 'value2',
}
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(mymodel, 'XmlModel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_object_is_xml(self):
"""Test serialize object as is.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<Data country="france">
<Age>37</Age>
</Data>""")
s = Serializer()
rawxml = s.body(basic_xml, 'object', is_xml=True)
# It should actually be the same object, should not even try to touch it
assert rawxml is basic_xml
@pytest.mark.skipif(sys.version_info < (3,6),
reason="Unstable before python3.6 for some reasons")
def test_type_basic_is_xml(self):
"""Test some types."""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<Data>
<Age>37</Age>
<Enabled>true</Enabled>
</Data>""")
class XmlModel(Model):
_attribute_map = {
'age': {'key': 'age', 'type': 'int', 'xml':{'name': 'Age'}},
'enabled': {'key': 'enabled', 'type': 'bool', 'xml':{'name': 'Enabled'}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
age=37,
enabled=True
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(mymodel, 'XmlModel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_direct_array_is_xml(self):
"""Test an ultra basic XML."""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<bananas>
<Data country="france"/>
</bananas>
""")
class XmlModel(Model):
_attribute_map = {
'country': {'key': 'country', 'type': 'str', 'xml':{'name': 'country', 'attr': True}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
country="france"
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(
[mymodel],
'[XmlModel]',
serialization_ctxt={'xml': {'name': 'bananas', 'wrapped': True}},
is_xml=True
)
assert_xml_equals(rawxml, basic_xml)
def test_list_wrapped_basic_types_is_xml(self):
"""Test XML list and wrap, items is basic type and there is no itemsName.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<GoodApples>
<GoodApples>granny</GoodApples>
<GoodApples>fuji</GoodApples>
</GoodApples>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
'good_apples': {'key': 'GoodApples', 'type': '[str]', 'xml': {'name': 'GoodApples', 'wrapped': True}},
}
_xml_map = {
'name': 'AppleBarrel'
}
mymodel = AppleBarrel(
good_apples=['granny', 'fuji']
)
s = Serializer({"AppleBarrel": AppleBarrel})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_list_not_wrapped_basic_types_is_xml(self):
"""Test XML list and no wrap, items is basic type and there is no itemsName.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<GoodApples>granny</GoodApples>
<GoodApples>fuji</GoodApples>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
'good_apples': {'key': 'GoodApples', 'type': '[str]', 'xml': {'name': 'GoodApples'}},
}
_xml_map = {
'name': 'AppleBarrel'
}
mymodel = AppleBarrel(
good_apples=['granny', 'fuji']
)
s = Serializer({"AppleBarrel": AppleBarrel})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_list_wrapped_items_name_complex_types_is_xml(self):
"""Test XML list and wrap, items is ref and there is itemsName.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<GoodApples>
<Apple name="granny"/>
<Apple name="fuji"/>
</GoodApples>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
# Pomme should be ignored, since it's invalid to define itemsName for a $ref type
'good_apples': {'key': 'GoodApples', 'type': '[Apple]', 'xml': {'name': 'GoodApples', 'wrapped': True, 'itemsName': 'Pomme'}},
}
_xml_map = {
'name': 'AppleBarrel'
}
class Apple(Model):
_attribute_map = {
'name': {'key': 'name', 'type': 'str', 'xml':{'name': 'name', 'attr': True}},
}
_xml_map = {
'name': 'Apple'
}
mymodel = AppleBarrel(
good_apples=[
Apple(name='granny'),
Apple(name='fuji')
]
)
s = Serializer({"AppleBarrel": AppleBarrel, "Apple": Apple})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_list_not_wrapped_items_name_complex_types_is_xml(self):
"""Test XML list and wrap, items is ref and there is itemsName.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<Apple name="granny"/>
<Apple name="fuji"/>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
# Pomme should be ignored, since it's invalid to define itemsName for a $ref type
'good_apples': {'key': 'GoodApples', 'type': '[Apple]', 'xml': {'name': 'GoodApples', 'itemsName': 'Pomme'}},
}
_xml_map = {
'name': 'AppleBarrel'
}
class Apple(Model):
_attribute_map = {
'name': {'key': 'name', 'type': 'str', 'xml':{'name': 'name', 'attr': True}},
}
_xml_map = {
'name': 'Apple'
}
mymodel = AppleBarrel(
good_apples=[
Apple(name='granny'),
Apple(name='fuji')
]
)
s = Serializer({"AppleBarrel": AppleBarrel, "Apple": Apple})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_list_wrapped_complex_types_is_xml(self):
"""Test XML list and wrap, items is ref and there is no itemsName.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<GoodApples>
<Apple name="granny"/>
<Apple name="fuji"/>
</GoodApples>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
'good_apples': {'key': 'GoodApples', 'type': '[Apple]', 'xml': {'name': 'GoodApples', 'wrapped': True}},
}
_xml_map = {
'name': 'AppleBarrel'
}
class Apple(Model):
_attribute_map = {
'name': {'key': 'name', 'type': 'str', 'xml':{'name': 'name', 'attr': True}},
}
_xml_map = {
'name': 'Apple'
}
mymodel = AppleBarrel(
good_apples=[
Apple(name='granny'),
Apple(name='fuji')
]
)
s = Serializer({"AppleBarrel": AppleBarrel, "Apple": Apple})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_list_not_wrapped_complex_types_is_xml(self):
"""Test XML list and wrap, items is ref and there is no itemsName.
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<Apple name="granny"/>
<Apple name="fuji"/>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
# Name is ignored if "wrapped" is False
'good_apples': {'key': 'GoodApples', 'type': '[Apple]', 'xml': {'name': 'GoodApples'}},
}
_xml_map = {
'name': 'AppleBarrel'
}
class Apple(Model):
_attribute_map = {
'name': {'key': 'name', 'type': 'str', 'xml':{'name': 'name', 'attr': True}},
}
_xml_map = {
'name': 'Apple'
}
mymodel = AppleBarrel(
good_apples=[
Apple(name='granny'),
Apple(name='fuji')
]
)
s = Serializer({"AppleBarrel": AppleBarrel, "Apple": Apple})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
@pytest.mark.skipif(sys.version_info < (3,6),
reason="Unstable before python3.6 for some reasons")
def test_two_complex_same_type_is_xml(self):
"""Two different attribute are same type
"""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<AppleBarrel>
<EuropeanApple name="granny"/>
<USAApple name="fuji"/>
</AppleBarrel>""")
class AppleBarrel(Model):
_attribute_map = {
'eu_apple': {'key': 'EuropeanApple', 'type': 'Apple', 'xml': {'name': 'EuropeanApple'}},
'us_apple': {'key': 'USAApple', 'type': 'Apple', 'xml': {'name': 'USAApple'}},
}
_xml_map = {
'name': 'AppleBarrel'
}
class Apple(Model):
_attribute_map = {
'name': {'key': 'name', 'type': 'str', 'xml':{'name': 'name', 'attr': True}},
}
_xml_map = {
}
mymodel = AppleBarrel(
eu_apple=Apple(name='granny'),
us_apple=Apple(name='fuji'),
)
s = Serializer({"AppleBarrel": AppleBarrel, "Apple": Apple})
rawxml = s.body(mymodel, 'AppleBarrel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
def test_basic_namespace_is_xml(self):
"""Test an ultra basic XML."""
basic_xml = ET.fromstring("""<?xml version="1.0"?>
<Data xmlns:fictional="http://characters.example.com">
<fictional:Age>37</fictional:Age>
</Data>""")
class XmlModel(Model):
_attribute_map = {
'age': {'key': 'age', 'type': 'int', 'xml':{'name': 'Age', 'prefix':'fictional','ns':'http://characters.example.com'}},
}
_xml_map = {
'name': 'Data'
}
mymodel = XmlModel(
age=37,
)
s = Serializer({"XmlModel": XmlModel})
rawxml = s.body(mymodel, 'XmlModel', is_xml=True)
assert_xml_equals(rawxml, basic_xml)
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment