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" % (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" % (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("") 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("") 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`.