Skip to content

Commit 9757d4c

Browse files
authored
Support XML attributes with namespaces (#209)
* Support XML attributes with namespaces * Serialize attributes with namespaces * Attr+ns serialization tests * Happy CI * Fix regression on NS lookup * ChangeLog
1 parent 66cd196 commit 9757d4c

File tree

3 files changed

+109
-23
lines changed

3 files changed

+109
-23
lines changed

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ To install:
2020
Release History
2121
---------------
2222

23+
2020-XX-XX Version 0.6.16
24+
+++++++++++++++++++++++++
25+
26+
**Bugfixes**
27+
28+
- Fix XML parsing with namespaces and attributes #209
29+
30+
**Features**
31+
32+
- Add py.typed for mypy support
33+
34+
2335
2020-06-04 Version 0.6.15
2436
+++++++++++++++++++++++++
2537

msrest/serialization.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,12 @@ def _serialize(self, target_obj, data_type=None, **kwargs):
519519
if is_xml_model_serialization:
520520
xml_desc = attr_desc.get('xml', {})
521521
xml_name = xml_desc.get('name', attr_desc['key'])
522+
xml_prefix = xml_desc.get('prefix', None)
523+
xml_ns = xml_desc.get('ns', None)
522524
if xml_desc.get("attr", False):
525+
if xml_ns:
526+
ET.register_namespace(xml_prefix, xml_ns)
527+
xml_name = "{{{}}}{}".format(xml_ns, xml_name)
523528
serialized.set(xml_name, new_attr)
524529
continue
525530
if isinstance(new_attr, list):
@@ -537,8 +542,8 @@ def _serialize(self, target_obj, data_type=None, **kwargs):
537542
# Integrate namespace if necessary
538543
local_node = _create_xml_node(
539544
xml_name,
540-
xml_desc.get('prefix', None),
541-
xml_desc.get('ns', None)
545+
xml_prefix,
546+
xml_ns
542547
)
543548
local_node.text = unicode_str(new_attr)
544549
serialized.append(local_node)
@@ -1205,11 +1210,8 @@ def _extract_name_from_internal_type(internal_type):
12051210
xml_name = internal_type_xml_map.get('name', internal_type.__name__)
12061211
xml_ns = internal_type_xml_map.get("ns", None)
12071212
if xml_ns:
1208-
ns = {'prefix': xml_ns}
1209-
xml_name = "prefix:"+xml_name
1210-
else:
1211-
ns = {} # And keep same xml_name
1212-
return xml_name, ns
1213+
xml_name = "{{{}}}{}".format(xml_ns, xml_name)
1214+
return xml_name
12131215

12141216

12151217
def xml_key_extractor(attr, attr_desc, data):
@@ -1223,10 +1225,6 @@ def xml_key_extractor(attr, attr_desc, data):
12231225
xml_desc = attr_desc.get('xml', {})
12241226
xml_name = xml_desc.get('name', attr_desc['key'])
12251227

1226-
# If it's an attribute, that's simple
1227-
if xml_desc.get("attr", False):
1228-
return data.get(xml_name)
1229-
12301228
# Look for a children
12311229
is_iter_type = attr_desc['type'].startswith("[")
12321230
is_wrapped = xml_desc.get("wrapped", False)
@@ -1236,28 +1234,29 @@ def xml_key_extractor(attr, attr_desc, data):
12361234
# Integrate namespace if necessary
12371235
xml_ns = xml_desc.get('ns', internal_type_xml_map.get("ns", None))
12381236
if xml_ns:
1239-
ns = {'prefix': xml_ns}
1240-
xml_name = "prefix:"+xml_name
1241-
else:
1242-
ns = {} # And keep same xml_name
1237+
xml_name = "{{{}}}{}".format(xml_ns, xml_name)
1238+
1239+
# If it's an attribute, that's simple
1240+
if xml_desc.get("attr", False):
1241+
return data.get(xml_name)
12431242

12441243
# Scenario where I take the local name:
12451244
# - Wrapped node
12461245
# - Internal type is an enum (considered basic types)
12471246
# - Internal type has no XML/Name node
12481247
if is_wrapped or (internal_type and (issubclass(internal_type, Enum) or 'name' not in internal_type_xml_map)):
1249-
children = data.findall(xml_name, ns)
1248+
children = data.findall(xml_name)
12501249
# If internal type has a local name and it's not a list, I use that name
12511250
elif not is_iter_type and internal_type and 'name' in internal_type_xml_map:
1252-
xml_name, ns = _extract_name_from_internal_type(internal_type)
1253-
children = data.findall(xml_name, ns)
1251+
xml_name = _extract_name_from_internal_type(internal_type)
1252+
children = data.findall(xml_name)
12541253
# That's an array
12551254
else:
12561255
if internal_type: # Complex type, ignore itemsName and use the complex type name
1257-
items_name, ns = _extract_name_from_internal_type(internal_type)
1256+
items_name = _extract_name_from_internal_type(internal_type)
12581257
else:
12591258
items_name = xml_desc.get("itemsName", xml_name)
1260-
children = data.findall(items_name, ns)
1259+
children = data.findall(items_name)
12611260

12621261
if len(children) == 0:
12631262
if is_iter_type:

tests/test_xml_serialization.py

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,21 +474,25 @@ class XmlModel(Model):
474474
def test_complex_namespace(self):
475475
"""Test recursive namespace."""
476476
basic_xml = """<?xml version="1.0"?>
477-
<entry xmlns="http://www.w3.org/2005/Atom">
477+
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
478478
<author>
479479
<name>lmazuel</name>
480480
</author>
481481
<AuthorizationRules xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect">
482-
<AuthorizationRule>
482+
<AuthorizationRule i:type="SharedAccessAuthorizationRule">
483483
<KeyName>testpolicy</KeyName>
484484
</AuthorizationRule>
485485
</AuthorizationRules>
486+
<CountDetails xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect">
487+
<d2p1:ActiveMessageCount xmlns:d2p1="http://schemas.microsoft.com/netservices/2011/06/servicebus">12</d2p1:ActiveMessageCount>
488+
</CountDetails>
486489
</entry>"""
487490

488491
class XmlRoot(Model):
489492
_attribute_map = {
490493
'author': {'key': 'author', 'type': 'QueueDescriptionResponseAuthor'},
491494
'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'}},
495+
'message_count_details': {'key': 'MessageCountDetails', 'type': 'MessageCountDetails'},
492496
}
493497
_xml_map = {
494498
'name': 'entry', 'ns': 'http://www.w3.org/2005/Atom'
@@ -504,22 +508,34 @@ class QueueDescriptionResponseAuthor(Model):
504508

505509
class AuthorizationRule(Model):
506510
_attribute_map = {
511+
'type': {'key': 'type', 'type': 'str', 'xml': {'attr': True, 'prefix': 'i', 'ns': 'http://www.w3.org/2001/XMLSchema-instance'}},
507512
'key_name': {'key': 'KeyName', 'type': 'str', 'xml': {'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'}},
508513
}
509514
_xml_map = {
510515
'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'
511516
}
512517

518+
class MessageCountDetails(Model):
519+
_attribute_map = {
520+
'active_message_count': {'key': 'ActiveMessageCount', 'type': 'int', 'xml': {'prefix': 'd2p1', 'ns': 'http://schemas.microsoft.com/netservices/2011/06/servicebus'}},
521+
}
522+
_xml_map = {
523+
'name': 'CountDetails', 'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'
524+
}
525+
513526

514527
s = Deserializer({
515528
"XmlRoot": XmlRoot,
516529
"QueueDescriptionResponseAuthor": QueueDescriptionResponseAuthor,
517530
"AuthorizationRule": AuthorizationRule,
531+
"MessageCountDetails": MessageCountDetails,
518532
})
519533
result = s(XmlRoot, basic_xml, "application/xml")
520534

521535
assert result.author.name == "lmazuel"
522536
assert result.authorization_rules[0].key_name == "testpolicy"
537+
assert result.authorization_rules[0].type == "SharedAccessAuthorizationRule"
538+
assert result.message_count_details.active_message_count == 12
523539

524540

525541
class TestXmlSerialization:
@@ -1409,4 +1425,63 @@ class XmlModel(Model):
14091425
s = Serializer({"XmlModel": XmlModel})
14101426
rawxml = s.body(mymodel, 'XmlModel', is_xml=True)
14111427

1412-
assert_xml_equals(rawxml, basic_xml)
1428+
assert_xml_equals(rawxml, basic_xml)
1429+
1430+
@pytest.mark.skipif(sys.version_info < (3,6),
1431+
reason="Unstable before python3.6 for some reasons")
1432+
def test_complex_namespace(self):
1433+
"""Test recursive namespace."""
1434+
basic_xml = ET.fromstring("""<?xml version="1.0"?>
1435+
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
1436+
<author>
1437+
<name>lmazuel</name>
1438+
</author>
1439+
<AuthorizationRules xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect">
1440+
<AuthorizationRule i:type="SharedAccessAuthorizationRule">
1441+
<KeyName>testpolicy</KeyName>
1442+
</AuthorizationRule>
1443+
</AuthorizationRules>
1444+
</entry>""")
1445+
1446+
class XmlRoot(Model):
1447+
_attribute_map = {
1448+
'author': {'key': 'author', 'type': 'QueueDescriptionResponseAuthor'},
1449+
'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'}},
1450+
}
1451+
_xml_map = {
1452+
'name': 'entry', 'ns': 'http://www.w3.org/2005/Atom'
1453+
}
1454+
1455+
class QueueDescriptionResponseAuthor(Model):
1456+
_attribute_map = {
1457+
'name': {'key': 'name', 'type': 'str', 'xml': {'ns': 'http://www.w3.org/2005/Atom'}},
1458+
}
1459+
_xml_map = {
1460+
'ns': 'http://www.w3.org/2005/Atom'
1461+
}
1462+
1463+
class AuthorizationRule(Model):
1464+
_attribute_map = {
1465+
'type': {'key': 'type', 'type': 'str', 'xml': {'attr': True, 'prefix': 'i', 'ns': 'http://www.w3.org/2001/XMLSchema-instance'}},
1466+
'key_name': {'key': 'KeyName', 'type': 'str', 'xml': {'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'}},
1467+
}
1468+
_xml_map = {
1469+
'ns': 'http://schemas.microsoft.com/netservices/2010/10/servicebus/connect'
1470+
}
1471+
1472+
mymodel = XmlRoot(
1473+
author = QueueDescriptionResponseAuthor(name = "lmazuel"),
1474+
authorization_rules = [AuthorizationRule(
1475+
type="SharedAccessAuthorizationRule",
1476+
key_name="testpolicy"
1477+
)]
1478+
)
1479+
1480+
s = Serializer({
1481+
"XmlRoot": XmlRoot,
1482+
"QueueDescriptionResponseAuthor": QueueDescriptionResponseAuthor,
1483+
"AuthorizationRule": AuthorizationRule,
1484+
})
1485+
rawxml = s.body(mymodel, 'XmlModel')
1486+
1487+
assert_xml_equals(rawxml, basic_xml)

0 commit comments

Comments
 (0)