diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst
index a1f334d4178e436..969941b5b2cbc75 100644
--- a/Doc/library/xml.dom.minidom.rst
+++ b/Doc/library/xml.dom.minidom.rst
@@ -132,7 +132,7 @@ module documentation. This section lists the differences between the API and
... # Work with dom.
-.. method:: Node.writexml(writer, indent="", addindent="", newl="")
+.. method:: Node.writexml(writer, indent="", addindent="", newl="", *, sort_attrs=True)
Write XML to the writer object. The writer should have a :meth:`write` method
which matches that of the file object interface. The *indent* parameter is the
@@ -143,11 +143,13 @@ module documentation. This section lists the differences between the API and
For the :class:`Document` node, an additional keyword argument *encoding* can
be used to specify the encoding field of the XML header.
+ If *sort_attrs* is true (by default), then element attributes will be
+ written in lexical order.
+
.. versionchanged:: 3.8
- The :meth:`writexml` method now preserves the attribute order specified
- by the user.
+ Added support for the *sort_attrs* argument.
-.. method:: Node.toxml(encoding=None)
+.. method:: Node.toxml(encoding=None, *, sort_attrs=True)
Return a string or byte string containing the XML represented by
the DOM node.
@@ -159,22 +161,23 @@ module documentation. This section lists the differences between the API and
encoding. Encoding this string in an encoding other than UTF-8 is
likely incorrect, since UTF-8 is the default encoding of XML.
+ The *sort_attrs* argument behaves like the corresponding argument of
+ :meth:`writexml`.
+
.. versionchanged:: 3.8
- The :meth:`toxml` method now preserves the attribute order specified
- by the user.
+ Added support for the *sort_attrs* argument.
-.. method:: Node.toprettyxml(indent="", newl="", encoding="")
+.. method:: Node.toprettyxml(indent="", newl="", encoding="", *, sort_attrs=True)
Return a pretty-printed version of the document. *indent* specifies the
indentation string and defaults to a tabulator; *newl* specifies the string
emitted at the end of each line and defaults to ``\n``.
- The *encoding* argument behaves like the corresponding argument of
- :meth:`toxml`.
+ The *encoding* and *sort_attrs* arguments behave like the corresponding
+ arguments of :meth:`toxml`.
.. versionchanged:: 3.8
- The :meth:`toprettyxml` method now preserves the attribute order specified
- by the user.
+ Added support for the *sort_attrs* argument.
.. _dom-example:
diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst
index aae80140374cc1e..723eaa6dc3d978a 100644
--- a/Doc/library/xml.etree.elementtree.rst
+++ b/Doc/library/xml.etree.elementtree.rst
@@ -592,29 +592,34 @@ Functions
.. function:: tostring(element, encoding="us-ascii", method="xml", *, \
- short_empty_elements=True)
+ short_empty_elements=True, sort_attrs=True)
Generates a string representation of an XML element, including all
subelements. *element* is an :class:`Element` instance. *encoding* [1]_ is
the output encoding (default is US-ASCII). Use ``encoding="unicode"`` to
generate a Unicode string (otherwise, a bytestring is generated). *method*
is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
- *short_empty_elements* has the same meaning as in :meth:`ElementTree.write`.
+ *short_empty_elements* and *sort_attrs* have the same meaning as in
+ :meth:`ElementTree.write`.
Returns an (optionally) encoded string containing the XML data.
.. versionadded:: 3.4
The *short_empty_elements* parameter.
+ .. versionadded:: 3.8
+ The *sort_attrs* parameter.
+
.. function:: tostringlist(element, encoding="us-ascii", method="xml", *, \
- short_empty_elements=True)
+ short_empty_elements=True, sort_attrs=True)
Generates a string representation of an XML element, including all
subelements. *element* is an :class:`Element` instance. *encoding* [1]_ is
the output encoding (default is US-ASCII). Use ``encoding="unicode"`` to
generate a Unicode string (otherwise, a bytestring is generated). *method*
is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
- *short_empty_elements* has the same meaning as in :meth:`ElementTree.write`.
+ *short_empty_elements* and *sort_attrs* have the same meaning as in
+ :meth:`ElementTree.write`.
Returns a list of (optionally) encoded strings containing the XML data.
It does not guarantee any specific sequence, except that
``b"".join(tostringlist(element)) == tostring(element)``.
@@ -624,6 +629,9 @@ Functions
.. versionadded:: 3.4
The *short_empty_elements* parameter.
+ .. versionadded:: 3.8
+ The *sort_attrs* parameter.
+
.. function:: XML(text, parser=None)
@@ -925,7 +933,7 @@ ElementTree Objects
.. method:: write(file, encoding="us-ascii", xml_declaration=None, \
default_namespace=None, method="xml", *, \
- short_empty_elements=True)
+ short_empty_elements=True, sort_attrs=True)
Writes the element tree to a file, as XML. *file* is a file name, or a
:term:`file object` opened for writing. *encoding* [1]_ is the output
@@ -940,6 +948,8 @@ ElementTree Objects
of elements that contain no content. If ``True`` (the default), they are
emitted as a single self-closed tag, otherwise they are emitted as a pair
of start/end tags.
+ If *sort_attrs* is true (by default), then element attributes will be
+ written in lexical order.
The output is either a string (:class:`str`) or binary (:class:`bytes`).
This is controlled by the *encoding* argument. If *encoding* is
@@ -951,9 +961,8 @@ ElementTree Objects
.. versionadded:: 3.4
The *short_empty_elements* parameter.
- .. versionchanged:: 3.8
- The :meth:`write` method now preserves the attribute order specified
- by the user.
+ .. versionadded:: 3.8
+ The *sort_attrs* parameter.
This is the XML file that is going to be manipulated::
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
index ad5be2f0f0e2732..ba6ddb6a29cdd11 100644
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -1567,17 +1567,26 @@ def test_minidom_attribute_order(self):
doc = parseString(xml_str)
output = io.StringIO()
doc.writexml(output)
+ self.assertEqual(output.getvalue(),
+ '')
+ output = io.StringIO()
+ doc.writexml(output, sort_attrs=False)
self.assertEqual(output.getvalue(), xml_str)
def test_toxml_with_attributes_ordered(self):
xml_str = ''
doc = parseString(xml_str)
- self.assertEqual(doc.toxml(), xml_str)
+ self.assertEqual(doc.toxml(),
+ '')
+ self.assertEqual(doc.toxml(sort_attrs=False), xml_str)
def test_toprettyxml_with_attributes_ordered(self):
xml_str = ''
doc = parseString(xml_str)
self.assertEqual(doc.toprettyxml(),
+ '\n'
+ '\n')
+ self.assertEqual(doc.toprettyxml(sort_attrs=False),
'\n'
'\n')
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 8a7ec0076ff0368..d577d488a1a214d 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -1054,10 +1054,15 @@ def test_dump_attribute_order(self):
def test_tree_write_attribute_order(self):
# See BPO 34160
- root = ET.Element('cirriculum', status='public', company='example')
+ root = ET.XML('')
+ #root = ET.Element('cirriculum', status='public', company='example')
self.assertEqual(serialize(root),
+ '')
+ self.assertEqual(serialize(root, sort_attrs=False),
'')
self.assertEqual(serialize(root, method='html'),
+ '')
+ self.assertEqual(serialize(root, method='html', sort_attrs=False),
'')
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index 469c51735e0385b..7481c47bf007def 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -43,10 +43,10 @@ class Node(xml.dom.Node):
def __bool__(self):
return True
- def toxml(self, encoding=None):
- return self.toprettyxml("", "", encoding)
+ def toxml(self, encoding=None, *, sort_attrs=True):
+ return self.toprettyxml("", "", encoding, sort_attrs=sort_attrs)
- def toprettyxml(self, indent="\t", newl="\n", encoding=None):
+ def toprettyxml(self, indent="\t", newl="\n", encoding=None, *, sort_attrs=True):
if encoding is None:
writer = io.StringIO()
else:
@@ -56,9 +56,11 @@ def toprettyxml(self, indent="\t", newl="\n", encoding=None):
newline='\n')
if self.nodeType == Node.DOCUMENT_NODE:
# Can pass encoding only to document, to put it into XML header
- self.writexml(writer, "", indent, newl, encoding)
+ self.writexml(writer, "", indent, newl, encoding,
+ sort_attrs=sort_attrs)
else:
- self.writexml(writer, "", indent, newl)
+ self.writexml(writer, "", indent, newl,
+ sort_attrs=sort_attrs)
if encoding is None:
return writer.getvalue()
else:
@@ -847,15 +849,19 @@ def getElementsByTagNameNS(self, namespaceURI, localName):
def __repr__(self):
return "" % (self.tagName, id(self))
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ sort_attrs=True):
# indent = current indentation
# addindent = indentation to add to higher levels
# newl = newline string
writer.write(indent+"<" + self.tagName)
attrs = self._get_attributes()
+ a_names = attrs.keys()
+ if sort_attrs:
+ a_names = sorted(a_names)
- for a_name in attrs.keys():
+ for a_name in a_names:
writer.write(" %s=\"" % a_name)
_write_data(writer, attrs[a_name].value)
writer.write("\"")
@@ -863,11 +869,13 @@ def writexml(self, writer, indent="", addindent="", newl=""):
writer.write(">")
if (len(self.childNodes) == 1 and
self.childNodes[0].nodeType == Node.TEXT_NODE):
- self.childNodes[0].writexml(writer, '', '', '')
+ self.childNodes[0].writexml(writer, '', '', '',
+ sort_attrs=sort_attrs)
else:
writer.write(newl)
for node in self.childNodes:
- node.writexml(writer, indent+addindent, addindent, newl)
+ node.writexml(writer, indent+addindent, addindent, newl,
+ sort_attrs=sort_attrs)
writer.write(indent)
writer.write("%s>%s" % (self.tagName, newl))
else:
@@ -984,7 +992,8 @@ def _set_nodeName(self, value):
self.target = value
nodeName = property(_get_nodeName, _set_nodeName)
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ sort_attrs=True):
writer.write("%s%s %s?>%s" % (indent,self.target, self.data, newl))
@@ -1084,7 +1093,8 @@ def splitText(self, offset):
self.data = self.data[:offset]
return newText
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ sort_attrs=True):
_write_data(writer, "%s%s%s" % (indent, self.data, newl))
# DOM Level 3 (WD 9 April 2002)
@@ -1179,7 +1189,8 @@ def __init__(self, data):
CharacterData.__init__(self)
self._data = data
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ sort_attrs=True):
if "--" in self.data:
raise ValueError("'--' is not allowed in a comment node")
writer.write("%s%s" % (indent, self.data, newl))
@@ -1191,7 +1202,8 @@ class CDATASection(Text):
nodeType = Node.CDATA_SECTION_NODE
nodeName = "#cdata-section"
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ sort_attrs=True):
if self.data.find("]]>") >= 0:
raise ValueError("']]>' not allowed in a CDATA section")
writer.write("" % self.data)
@@ -1324,7 +1336,8 @@ def cloneNode(self, deep):
else:
return None
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ sort_attrs=True):
writer.write("'+newl)
else:
writer.write('%s' % (
encoding, newl))
for node in self.childNodes:
- node.writexml(writer, indent, addindent, newl)
+ node.writexml(writer, indent, addindent, newl, sort_attrs=sort_attrs)
# DOM Level 3 (WD 9 April 2002)
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index c1cf483cf56bb26..055d1a8f5256c6e 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -721,7 +721,8 @@ def write(self, file_or_filename,
xml_declaration=None,
default_namespace=None,
method=None, *,
- short_empty_elements=True):
+ short_empty_elements=True,
+ sort_attrs=True):
"""Write element tree to a file as XML.
Arguments:
@@ -744,6 +745,7 @@ def write(self, file_or_filename,
tag, otherwise they are emitted as a pair
of start/end tags
+ *sort_attrs* -- output attributes in lexical order
"""
if not method:
method = "xml"
@@ -772,7 +774,8 @@ def write(self, file_or_filename,
qnames, namespaces = _namespaces(self._root, default_namespace)
serialize = _serialize[method]
serialize(write, self._root, qnames, namespaces,
- short_empty_elements=short_empty_elements)
+ short_empty_elements=short_empty_elements,
+ sort_attrs=sort_attrs)
def write_c14n(self, file):
# lxml.etree compatibility. use output method instead
@@ -895,7 +898,8 @@ def add_qname(qname):
return qnames, namespaces
def _serialize_xml(write, elem, qnames, namespaces,
- short_empty_elements, **kwargs):
+ short_empty_elements, *,
+ sort_attrs=True, **kwargs):
tag = elem.tag
text = elem.text
if tag is Comment:
@@ -909,7 +913,8 @@ def _serialize_xml(write, elem, qnames, namespaces,
write(_escape_cdata(text))
for e in elem:
_serialize_xml(write, e, qnames, None,
- short_empty_elements=short_empty_elements)
+ short_empty_elements=short_empty_elements,
+ sort_attrs=sort_attrs)
else:
write("<" + tag)
items = list(elem.items())
@@ -923,6 +928,8 @@ def _serialize_xml(write, elem, qnames, namespaces,
k,
_escape_attrib(v)
))
+ if sort_attrs:
+ items = sorted(items)
for k, v in items:
if isinstance(k, QName):
k = k.text
@@ -937,7 +944,8 @@ def _serialize_xml(write, elem, qnames, namespaces,
write(_escape_cdata(text))
for e in elem:
_serialize_xml(write, e, qnames, None,
- short_empty_elements=short_empty_elements)
+ short_empty_elements=short_empty_elements,
+ sort_attrs=sort_attrs)
write("" + tag + ">")
else:
write(" />")
@@ -952,7 +960,8 @@ def _serialize_xml(write, elem, qnames, namespaces,
except NameError:
pass
-def _serialize_html(write, elem, qnames, namespaces, **kwargs):
+def _serialize_html(write, elem, qnames, namespaces, *,
+ sort_attrs=True, **kwargs):
tag = elem.tag
text = elem.text
if tag is Comment:
@@ -965,7 +974,7 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs):
if text:
write(_escape_cdata(text))
for e in elem:
- _serialize_html(write, e, qnames, None)
+ _serialize_html(write, e, qnames, None, sort_attrs=sort_attrs)
else:
write("<" + tag)
items = list(elem.items())
@@ -979,6 +988,8 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs):
k,
_escape_attrib(v)
))
+ if sort_attrs:
+ items = sorted(items)
for k, v in items:
if isinstance(k, QName):
k = k.text
@@ -996,7 +1007,7 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs):
else:
write(_escape_cdata(text))
for e in elem:
- _serialize_html(write, e, qnames, None)
+ _serialize_html(write, e, qnames, None, sort_attrs=sort_attrs)
if ltag not in HTML_EMPTY:
write("" + tag + ">")
if elem.tail:
@@ -1116,7 +1127,7 @@ def _escape_attrib_html(text):
# --------------------------------------------------------------------
def tostring(element, encoding=None, method=None, *,
- short_empty_elements=True):
+ short_empty_elements=True, sort_attrs=True):
"""Generate string representation of XML element.
All subelements are included. If encoding is "unicode", a string
@@ -1131,7 +1142,8 @@ def tostring(element, encoding=None, method=None, *,
"""
stream = io.StringIO() if encoding == 'unicode' else io.BytesIO()
ElementTree(element).write(stream, encoding, method=method,
- short_empty_elements=short_empty_elements)
+ short_empty_elements=short_empty_elements,
+ sort_attrs=sort_attrs)
return stream.getvalue()
class _ListDataStream(io.BufferedIOBase):
@@ -1152,7 +1164,7 @@ def tell(self):
return len(self.lst)
def tostringlist(element, encoding=None, method=None, *,
- short_empty_elements=True):
+ short_empty_elements=True, sort_attrs=True):
lst = []
stream = _ListDataStream(lst)
ElementTree(element).write(stream, encoding, method=method,
@@ -1173,7 +1185,7 @@ def dump(elem):
# debugging
if not isinstance(elem, ElementTree):
elem = ElementTree(elem)
- elem.write(sys.stdout, encoding="unicode")
+ elem.write(sys.stdout, encoding="unicode", sort_attrs=False)
tail = elem.getroot().tail
if not tail or tail[-1] != "\n":
sys.stdout.write("\n")
diff --git a/Misc/NEWS.d/next/Library/2018-10-27-21-11-42.bpo-34160.UzyPZf.rst b/Misc/NEWS.d/next/Library/2018-10-27-21-11-42.bpo-34160.UzyPZf.rst
index 775d33aacda7aa7..ccedb3c05a617d5 100644
--- a/Misc/NEWS.d/next/Library/2018-10-27-21-11-42.bpo-34160.UzyPZf.rst
+++ b/Misc/NEWS.d/next/Library/2018-10-27-21-11-42.bpo-34160.UzyPZf.rst
@@ -1 +1,2 @@
-ElementTree and minidom now preserve the attribute order specified by the user.
+Added support for the *sort_attrs* argument in methods for serializing XML in
+modules :mod:`xml.etree.ElementTree` and :mod:`xml.dom.minidom`.