Can ElementTree be told to preserve the order of attributes?

Question:

I’ve written a fairly simple filter in python using ElementTree to munge the contexts of some xml files. And it works, more or less.

But it reorders the attributes of various tags, and I’d like it to not do that.

Does anyone know a switch I can throw to make it keep them in specified order?

Context for this

I’m working with and on a particle physics tool that has a complex, but oddly limited configuration system based on xml files. Among the many things setup that way are the paths to various static data files. These paths are hardcoded into the existing xml and there are no facilities for setting or varying them based on environment variables, and in our local installation they are necessarily in a different place.

This isn’t a disaster because the combined source- and build-control tool we’re using allows us to shadow certain files with local copies. But even thought the data fields are static the xml isn’t, so I’ve written a script for fixing the paths, but with the attribute rearrangement diffs between the local and master versions are harder to read than necessary.


This is my first time taking ElementTree for a spin (and only my fifth or sixth python project) so maybe I’m just doing it wrong.

Abstracted for simplicity the code looks like this:

tree = elementtree.ElementTree.parse(inputfile)
i = tree.getiterator()
for e in i:
    e.text = filter(e.text)
tree.write(outputfile)

Reasonable or dumb?


Related links:

Answers:

Nope. ElementTree uses a dictionary to store attribute values, so it’s inherently unordered.

Even DOM doesn’t guarantee you attribute ordering, and DOM exposes a lot more detail of the XML infoset than ElementTree does. (There are some DOMs that do offer it as a feature, but it’s not standard.)

Can it be fixed? Maybe. Here’s a stab at it that replaces the dictionary when parsing with an ordered one (collections.OrderedDict()).

from xml.etree import ElementTree
from collections import OrderedDict
import StringIO

class OrderedXMLTreeBuilder(ElementTree.XMLTreeBuilder):
    def _start_list(self, tag, attrib_in):
        fixname = self._fixname
        tag = fixname(tag)
        attrib = OrderedDict()
        if attrib_in:
            for i in range(0, len(attrib_in), 2):
                attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1])
        return self._target.start(tag, attrib)

>>> xmlf = StringIO.StringIO('<a b="c" d="e" f="g" j="k" h="i"/>')

>>> tree = ElementTree.ElementTree()
>>> root = tree.parse(xmlf, OrderedXMLTreeBuilder())
>>> root.attrib
OrderedDict([('b', 'c'), ('d', 'e'), ('f', 'g'), ('j', 'k'), ('h', 'i')])

Looks potentially promising.

>>> s = StringIO.StringIO()
>>> tree.write(s)
>>> s.getvalue()
'<a b="c" d="e" f="g" h="i" j="k" />'

Bah, the serialiser outputs them in canonical order.

This looks like the line to blame, in ElementTree._write:

            items.sort() # lexical order

Subclassing or monkey-patching that is going to be annoying as it’s right in the middle of a big method.

Unless you did something nasty like subclass OrderedDict and hack items to return a special subclass of list that ignores calls to sort(). Nah, probably that’s even worse and I should go to bed before I come up with anything more horrible than that.

Answered By: bobince

Wrong question. Should be: “Where do I find a diff gadget that works sensibly with XML files?

Answer: Google is your friend. First result for search on “xml diff” => this. There are a few more possibles.

Answered By: John Machin

From section 3.1 of the XML recommendation:

Note that the order of attribute specifications in a start-tag or empty-element tag is not significant.

Any system that relies on the order of attributes in an XML element is going to break.

Answered By: Robert Rossney

Have had your problem. Firstly looked for some Python script to canonize, didnt found anyone. Then started thinking about making one. Finally xmllintsolved.

Answered By: 1737973

With help from @bobince’s answer and these two (setting attribute order, overriding module methods)

I managed to get this monkey patched it’s dirty and I’d suggest using another module that better handles this scenario but when that isn’t a possibility:

# =======================================================================
# Monkey patch ElementTree
import xml.etree.ElementTree as ET

def _serialize_xml(write, elem, encoding, qnames, namespaces):
    tag = elem.tag
    text = elem.text
    if tag is ET.Comment:
        write("<!--%s-->" % ET._encode(text, encoding))
    elif tag is ET.ProcessingInstruction:
        write("<?%s?>" % ET._encode(text, encoding))
    else:
        tag = qnames[tag]
        if tag is None:
            if text:
                write(ET._escape_cdata(text, encoding))
            for e in elem:
                _serialize_xml(write, e, encoding, qnames, None)
        else:
            write("<" + tag)
            items = elem.items()
            if items or namespaces:
                if namespaces:
                    for v, k in sorted(namespaces.items(),
                                       key=lambda x: x[1]):  # sort on prefix
                        if k:
                            k = ":" + k
                        write(" names[k], v))
            if text or len(elem):
                write(">")
                if text:
                    write(ET._escape_cdata(text, encoding))
                for e in elem:
                    _serialize_xml(write, e, encoding, qnames, None)
                write("</" + tag + ">")
            else:
                write(" />")
    if elem.tail:
        write(ET._escape_cdata(elem.tail, encoding))

ET._serialize_xml = _serialize_xml

from collections import OrderedDict

class OrderedXMLTreeBuilder(ET.XMLTreeBuilder):
    def _start_list(self, tag, attrib_in):
        fixname = self._fixname
        tag = fixname(tag)
        attrib = OrderedDict()
        if attrib_in:
            for i in range(0, len(attrib_in), 2):
                attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1])
        return self._target.start(tag, attrib)

# =======================================================================

Then in your code:

tree = ET.parse(pathToFile, OrderedXMLTreeBuilder())
Answered By: SnellyBigoda

Yes, with lxml

>>> from lxml import etree
>>> root = etree.Element("root", interesting="totally")
>>> etree.tostring(root)
b'<root interesting="totally"/>'
>>> print(root.get("hello"))
None
>>> root.set("hello", "Huhu")
>>> print(root.get("hello"))
Huhu
>>> etree.tostring(root)
b'<root interesting="totally" hello="Huhu"/>'

Here is direct link to documentation, from which the above example is slightly adapted.

Also note that lxml has, by design, some good API compatiblity with standard xml.etree.ElementTree

Answered By: thdox

This is a partial solution, for the case where xml is being emitted and a predictable order is desired. It does not solve round trip parsing and writing. Both 2.7 and 3.x use sorted() to force an attribute ordering. So, this code, in conjunction with use of an OrderedDictionary to hold the attributes will preserve the order for xml output to match the order used to create the Elements.

from collections import OrderedDict
from xml.etree import ElementTree as ET

# Make sorted() a no-op for the ElementTree module
ET.sorted = lambda x: x

try:
    # python3 use a cPython implementation by default, prevent that
    ET.Element = ET._Element_Py
    # similarly, override SubElement method if desired
    def SubElement(parent, tag, attrib=OrderedDict(), **extra):
        attrib = attrib.copy()
        attrib.update(extra)
        element = parent.makeelement(tag, attrib)
        parent.append(element)
        return element
    ET.SubElement = SubElement
except AttributeError:
    pass  # nothing else for python2, ElementTree is pure python

# Make an element with a particular "meaningful" ordering
t = ET.ElementTree(ET.Element('component',
                       OrderedDict([('grp','foo'),('name','bar'),
                                    ('class','exec'),('arch','x86')])))
# Add a child element
ET.SubElement(t.getroot(),'depend',
              OrderedDict([('grp','foo'),('name','util1'),('class','lib')]))  
x = ET.tostring(n)
print (x)
# Order maintained...
# <component grp="foo" name="bar" class="exec" arch="x86"><depend grp="foo" name="util1" class="lib" /></component>

# Parse again, won't be ordered because Elements are created
#   without ordered dict
print ET.tostring(ET.fromstring(x))
# <component arch="x86" name="bar" grp="foo" class="exec"><depend name="util1" grp="foo" class="lib" /></component>

The problem with parsing XML into an element tree is that the code internally creates plain dicts which are passed in to Element(), at which point the order is lost. No equivalent simple patch is possible.

Answered By: Marvin

Best Option is to use the lxml library http://lxml.de/
Installing the lxml and just switching the library did the magic to me.

#import xml.etree.ElementTree as ET
from lxml import etree as ET
Answered By: Dinesh Jeyasankar

I used the accepted answer above, with both statements:

ET._serialize_xml = _serialize_xml
ET._serialize['xml'] = _serialize_xml

While this fixed the ordering in every node, attribute ordering on new nodes inserted from copies of existing nodes failed to preserve without a deepcopy. Watch out for reusing nodes to create others…
In my case I had an element with several attributes, so I wanted to reuse them:

to_add = ET.fromstring(ET.tostring(contract))
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)

The fromstring(tostring) will reorder the attributes in memory. It may not result in the alpha sorted dict of attributes, but it also may not have the expected ordering.

to_add = copy.deepcopy(contract)
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)

Now the ordering persists.

Answered By: TinCupChalice

This has been “fixed” in python 3.8. I can’t find any notes about it anywhere, but it works now.

D:tmpetree_order>type etree_order.py
import xml.etree.ElementTree as ET

a = ET.Element('a', {"aaa": "1", "ccc": "3", "bbb": "2"})

print(ET.tostring(a))
D:tmpetree_order>C:Python37-64python.exe etree_order.py
b'<a aaa="1" bbb="2" ccc="3" />'

D:tmpetree_order>c:Python38-64python.exe etree_order.py
b'<a aaa="1" ccc="3" bbb="2" />'
Answered By: teeks99

By running the python script in python 3.8 version we can preserve the order of the attributes in xml files.

Answered By: Hrithik Diwakar

I would recommend using LXML (as others have as well). If you need to preserve the order of attributes to adhere to the c14n v1 or v2 standards (https://www.w3.org/TR/xml-c14n2/) (i.e. increasing lexicographic order), lxml supports this very nicely by passing an output method (see heading C14N of https://lxml.de/api.html)

For example:

from lxml import etree as ET 
element = ET.Element('Test', B='beta', Z='omega', A='alpha') 
val = ET.tostring(element, method="c14n") 
print(val)
Answered By: John Bowers
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.