diff --git a/README.rst b/README.rst index 26254fd42b..9e0191f74b 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,18 @@ To install: Release History --------------- +2020-XX-XX Version 0.6.16 ++++++++++++++++++++++++++ + +**Bugfixes** + +- Fix XML parsing with namespaces and attributes #209 + +**Features** + +- Add py.typed for mypy support + + 2020-06-04 Version 0.6.15 +++++++++++++++++++++++++ diff --git a/msrest/serialization.py b/msrest/serialization.py index a8abd9a9ec..a21ee4b941 100644 --- a/msrest/serialization.py +++ b/msrest/serialization.py @@ -519,7 +519,12 @@ def _serialize(self, target_obj, data_type=None, **kwargs): if is_xml_model_serialization: xml_desc = attr_desc.get('xml', {}) xml_name = xml_desc.get('name', attr_desc['key']) + xml_prefix = xml_desc.get('prefix', None) + xml_ns = xml_desc.get('ns', None) if xml_desc.get("attr", False): + if xml_ns: + ET.register_namespace(xml_prefix, xml_ns) + xml_name = "{{{}}}{}".format(xml_ns, xml_name) serialized.set(xml_name, new_attr) continue if isinstance(new_attr, list): @@ -537,8 +542,8 @@ def _serialize(self, target_obj, data_type=None, **kwargs): # Integrate namespace if necessary local_node = _create_xml_node( xml_name, - xml_desc.get('prefix', None), - xml_desc.get('ns', None) + xml_prefix, + xml_ns ) local_node.text = unicode_str(new_attr) serialized.append(local_node) @@ -1205,11 +1210,8 @@ def _extract_name_from_internal_type(internal_type): xml_name = internal_type_xml_map.get('name', internal_type.__name__) xml_ns = internal_type_xml_map.get("ns", None) if xml_ns: - ns = {'prefix': xml_ns} - xml_name = "prefix:"+xml_name - else: - ns = {} # And keep same xml_name - return xml_name, ns + xml_name = "{{{}}}{}".format(xml_ns, xml_name) + return xml_name def xml_key_extractor(attr, attr_desc, data): @@ -1223,10 +1225,6 @@ def xml_key_extractor(attr, attr_desc, data): xml_desc = attr_desc.get('xml', {}) xml_name = xml_desc.get('name', attr_desc['key']) - # If it's an attribute, that's simple - if xml_desc.get("attr", False): - return data.get(xml_name) - # Look for a children is_iter_type = attr_desc['type'].startswith("[") is_wrapped = xml_desc.get("wrapped", False) @@ -1236,28 +1234,29 @@ def xml_key_extractor(attr, attr_desc, data): # Integrate namespace if necessary xml_ns = xml_desc.get('ns', internal_type_xml_map.get("ns", None)) if xml_ns: - ns = {'prefix': xml_ns} - xml_name = "prefix:"+xml_name - else: - ns = {} # And keep same xml_name + xml_name = "{{{}}}{}".format(xml_ns, xml_name) + + # If it's an attribute, that's simple + if xml_desc.get("attr", False): + return data.get(xml_name) # Scenario where I take the local name: # - Wrapped node # - Internal type is an enum (considered basic types) # - Internal type has no XML/Name node if is_wrapped or (internal_type and (issubclass(internal_type, Enum) or 'name' not in internal_type_xml_map)): - children = data.findall(xml_name, ns) + children = data.findall(xml_name) # If internal type has a local name and it's not a list, I use that name elif not is_iter_type and internal_type and 'name' in internal_type_xml_map: - xml_name, ns = _extract_name_from_internal_type(internal_type) - children = data.findall(xml_name, ns) + xml_name = _extract_name_from_internal_type(internal_type) + children = data.findall(xml_name) # That's an array else: if internal_type: # Complex type, ignore itemsName and use the complex type name - items_name, ns = _extract_name_from_internal_type(internal_type) + items_name = _extract_name_from_internal_type(internal_type) else: items_name = xml_desc.get("itemsName", xml_name) - children = data.findall(items_name, ns) + children = data.findall(items_name) if len(children) == 0: if is_iter_type: diff --git a/tests/test_xml_serialization.py b/tests/test_xml_serialization.py index ac31b5c975..54e4ed6bad 100644 --- a/tests/test_xml_serialization.py +++ b/tests/test_xml_serialization.py @@ -474,21 +474,25 @@ class XmlModel(Model): def test_complex_namespace(self): """Test recursive namespace.""" basic_xml = """ - + lmazuel - + testpolicy + + 12 + """ class XmlRoot(Model): _attribute_map = { 'author': {'key': 'author', 'type': 'QueueDescriptionResponseAuthor'}, 'authorization_rules': {'key': 'AuthorizationRules', 'type': '[AuthorizationRule]', 'xml': {'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect', 'wrapped': True, 'itemsNs': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'}}, + 'message_count_details': {'key': 'MessageCountDetails', 'type': 'MessageCountDetails'}, } _xml_map = { 'name': 'entry', 'ns': 'http://www.w3.org/2005/Atom' @@ -504,22 +508,34 @@ class QueueDescriptionResponseAuthor(Model): class AuthorizationRule(Model): _attribute_map = { + 'type': {'key': 'type', 'type': 'str', 'xml': {'attr': True, 'prefix': 'i', 'ns': 'http://www.w3.org/2001/XMLSchema-instance'}}, 'key_name': {'key': 'KeyName', 'type': 'str', 'xml': {'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'}}, } _xml_map = { 'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect' } + class MessageCountDetails(Model): + _attribute_map = { + 'active_message_count': {'key': 'ActiveMessageCount', 'type': 'int', 'xml': {'prefix': 'd2p1', 'ns': 'http://schemas.microsoft.com/netservices/2011/06/servicebus'}}, + } + _xml_map = { + 'name': 'CountDetails', 'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect' + } + s = Deserializer({ "XmlRoot": XmlRoot, "QueueDescriptionResponseAuthor": QueueDescriptionResponseAuthor, "AuthorizationRule": AuthorizationRule, + "MessageCountDetails": MessageCountDetails, }) result = s(XmlRoot, basic_xml, "application/xml") assert result.author.name == "lmazuel" assert result.authorization_rules[0].key_name == "testpolicy" + assert result.authorization_rules[0].type == "SharedAccessAuthorizationRule" + assert result.message_count_details.active_message_count == 12 class TestXmlSerialization: @@ -1409,4 +1425,63 @@ class XmlModel(Model): s = Serializer({"XmlModel": XmlModel}) rawxml = s.body(mymodel, 'XmlModel', is_xml=True) - assert_xml_equals(rawxml, basic_xml) \ No newline at end of file + assert_xml_equals(rawxml, basic_xml) + + @pytest.mark.skipif(sys.version_info < (3,6), + reason="Unstable before python3.6 for some reasons") + def test_complex_namespace(self): + """Test recursive namespace.""" + basic_xml = ET.fromstring(""" + + + lmazuel + + + + testpolicy + + + """) + + class XmlRoot(Model): + _attribute_map = { + 'author': {'key': 'author', 'type': 'QueueDescriptionResponseAuthor'}, + 'authorization_rules': {'key': 'AuthorizationRules', 'type': '[AuthorizationRule]', 'xml': {'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect', 'wrapped': True, 'itemsNs': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'}}, + } + _xml_map = { + 'name': 'entry', 'ns': 'http://www.w3.org/2005/Atom' + } + + class QueueDescriptionResponseAuthor(Model): + _attribute_map = { + 'name': {'key': 'name', 'type': 'str', 'xml': {'ns': 'http://www.w3.org/2005/Atom'}}, + } + _xml_map = { + 'ns': 'http://www.w3.org/2005/Atom' + } + + class AuthorizationRule(Model): + _attribute_map = { + 'type': {'key': 'type', 'type': 'str', 'xml': {'attr': True, 'prefix': 'i', 'ns': 'http://www.w3.org/2001/XMLSchema-instance'}}, + 'key_name': {'key': 'KeyName', 'type': 'str', 'xml': {'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'}}, + } + _xml_map = { + 'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect' + } + + mymodel = XmlRoot( + author = QueueDescriptionResponseAuthor(name = "lmazuel"), + authorization_rules = [AuthorizationRule( + type="SharedAccessAuthorizationRule", + key_name="testpolicy" + )] + ) + + s = Serializer({ + "XmlRoot": XmlRoot, + "QueueDescriptionResponseAuthor": QueueDescriptionResponseAuthor, + "AuthorizationRule": AuthorizationRule, + }) + rawxml = s.body(mymodel, 'XmlModel') + + assert_xml_equals(rawxml, basic_xml)