diff --git a/pyxform/tests_v1/pyxform_test_case.py b/pyxform/tests_v1/pyxform_test_case.py index 024ec93cd..0a7143073 100644 --- a/pyxform/tests_v1/pyxform_test_case.py +++ b/pyxform/tests_v1/pyxform_test_case.py @@ -255,7 +255,7 @@ def check_content(content): "Invalid parameter: 'body__contains'." "Use 'xml__contains' instead" ) - # preserve attribute ordering across all Python versions before writing to string + # guarantee that strings contain alphanumerically sorted attributes across Python versions reorder_attributes(root) for code in ["xml", "instance", "model", "itext"]: @@ -337,17 +337,23 @@ def assertNotContains(self, content, text, msg_prefix=""): def reorder_attributes(root): """ - Forces alphabetical ordering of all XML attributes to match pre Python 3.8 behavior. - In general, we should not rely on ordering, but changing all the tests is not - realistic at this moment. + Forces alphabetical ordering of all XML attributes to match pre Python 3.8 behavior. + In general, we should not rely on ordering, but changing all the tests is not + realistic at this moment. - See bottom of https://docs.python.org/3/library/xml.etree.elementtree.html#element-objects and - https://github.com/python/cpython/commit/a3697db0102b9b6747fe36009e42f9b08f0c1ea8 for more information. - """ + See bottom of https://docs.python.org/3/library/xml.etree.elementtree.html#element-objects and + https://github.com/python/cpython/commit/a3697db0102b9b6747fe36009e42f9b08f0c1ea8 for more information. + + NOTE: there's a similar ordering change made in utils.node. This one is also needed because in + assertPyxformXform, the survey is converted to XML and then read back in using ETree.fromstring. This + means that attribute ordering here is based on the attribute representation of xml.etree.ElementTree objects. + In utils.node, it is based on xml.dom.minidom.Element objects. See https://github.com/XLSForm/pyxform/issues/414. + """ for el in root.iter(): attrib = el.attrib if len(attrib) > 1: - # adjust attribute order, e.g. by sorting + # Sort attributes. Attributes are represented as {namespace}name so attributes with explicit + # namespaces will always sort after those without explicit namespaces. attribs = sorted(attrib.items()) attrib.clear() attrib.update(attribs) diff --git a/pyxform/utils.py b/pyxform/utils.py index b81ecbdb8..b3986fb98 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -101,9 +101,11 @@ def node(*args, **kwargs): unicode_args = [u for u in args if type(u) == unicode] assert len(unicode_args) <= 1 parsed_string = False - # kwargs is an xml attribute dictionary, - # here we convert it to a xml.dom.minidom.Element - for k, v in iter(kwargs.items()): + + # Convert the kwargs xml attribute dictionary to a xml.dom.minidom.Element. Sort the + # attributes to guarantee a consistent order across Python versions. + # See pyxform_test_case.reorder_attributes for details. + for k, v in iter(sorted(kwargs.items())): if k in blocked_attributes: continue if k == "toParseString":