https://github.com/python/cpython/commit/33083601f50d50e71d0f96e0fa7cc0c41d15e218
commit: 33083601f50d50e71d0f96e0fa7cc0c41d15e218
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-13T18:09:47+03:00
summary:
gh-81623: Do not add whitespace to significant content when pretty-printing
(GH-156660)
Pretty-printing added whitespace inside an element which contains text,
which changed its content. Now xml.dom.minidom.Node.toprettyxml() and
xml.etree.ElementTree.indent() do not add whitespace inside an element
which is marked with xml:space="preserve" or which contains text.
toprettyxml() also takes into account the content model declared in
the DTD: only white space in element content is ignorable.
files:
A Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst
M Doc/library/xml.dom.minidom.rst
M Doc/library/xml.etree.elementtree.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_minidom.py
M Lib/test/test_xml_etree.py
M Lib/xml/dom/minidom.py
M Lib/xml/etree/ElementTree.py
diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst
index efc81f31e36a5be..eb4984e2b53d929 100644
--- a/Doc/library/xml.dom.minidom.rst
+++ b/Doc/library/xml.dom.minidom.rst
@@ -187,6 +187,12 @@ module documentation. This section lists the differences
between the API and
The *standalone* argument behaves exactly as in :meth:`writexml`.
+ No indentation is added inside an element
+ which is marked with ``xml:space="preserve"``,
+ which is declared in the DTD as not having element content,
+ or, in absence of such declaration, which contains text,
+ because this would change its content.
+
.. versionchanged:: 3.8
The :meth:`toprettyxml` method now preserves the attribute order
specified
by the user.
@@ -194,6 +200,10 @@ module documentation. This section lists the differences
between the API and
.. versionchanged:: 3.9
The *standalone* parameter was added.
+ .. versionchanged:: next
+ Whitespace is no longer added inside an element with mixed content
+ or marked with ``xml:space="preserve"``.
+
.. _dom-example:
DOM Example
diff --git a/Doc/library/xml.etree.elementtree.rst
b/Doc/library/xml.etree.elementtree.rst
index 4f2497c8246be30..a61fb05bf99d873 100644
--- a/Doc/library/xml.etree.elementtree.rst
+++ b/Doc/library/xml.etree.elementtree.rst
@@ -631,8 +631,16 @@ Functions
characters by default. For indenting partial subtrees inside of an
already indented tree, pass the initial indentation level as *level*.
+ No whitespace is added inside an element
+ which is marked with ``xml:space="preserve"``
+ or which contains text, because this would change its content.
+
.. versionadded:: 3.9
+ .. versionchanged:: next
+ Whitespace is no longer added inside an element with mixed content
+ or marked with ``xml:space="preserve"``.
+
.. function:: iselement(element)
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 2eccc8e35605596..71ad3558b3be68f 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -702,6 +702,14 @@ xml
and :meth:`!Document.createEntityReference`.
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
+* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
+ and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
+ no longer add whitespace inside an element
+ which is marked with ``xml:space="preserve"`` or which contains text.
+ :meth:`!toprettyxml` also takes into account
+ the content model declared in the DTD.
+ (Contributed by Serhiy Storchaka in :gh:`81623`.)
+
* Add :meth:`!GetSpecifiedAttributeCount` method
to the :mod:`XML parser <xml.parsers.expat>` objects.
It tells how many of the reported attributes were given in the start tag
@@ -947,6 +955,15 @@ that may require changes to your code.
Attributes defaulted in the DTD are no longer omitted when parsing.
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
+* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
+ and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
+ no longer add whitespace inside an element
+ which is marked with ``xml:space="preserve"`` or which contains text,
+ because this changed the content of the element.
+ :meth:`!toprettyxml` also takes into account
+ the content model declared in the DTD.
+ (Contributed by Serhiy Storchaka in :gh:`81623`.)
+
* On Windows, seeking a pipe now fails instead of silently appearing to
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
index 2a7c3293174a249..837a54a64379f74 100644
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -617,32 +617,76 @@ def testAltNewline(self):
self.assertEqual(domstr, str.replace("\n", "\r\n"))
def test_toprettyxml_with_text_nodes(self):
- # see issue #4147, text nodes are not indented
+ # see gh-48397 and gh-81623,
+ # the content of an element with text is not changed
decl = '<?xml version="1.0" ?>\n'
self.assertEqual(parseString('<B>A</B>').toprettyxml(),
decl + '<B>A</B>\n')
self.assertEqual(parseString('<C>A<B>A</B></C>').toprettyxml(),
- decl + '<C>\n\tA\n\t<B>A</B>\n</C>\n')
+ decl + '<C>A<B>A</B></C>\n')
self.assertEqual(parseString('<C><B>A</B>A</C>').toprettyxml(),
- decl + '<C>\n\t<B>A</B>\n\tA\n</C>\n')
+ decl + '<C><B>A</B>A</C>\n')
self.assertEqual(parseString('<C><B>A</B><B>A</B></C>').toprettyxml(),
decl + '<C>\n\t<B>A</B>\n\t<B>A</B>\n</C>\n')
self.assertEqual(parseString('<C><B>A</B>A<B>A</B></C>').toprettyxml(),
- decl + '<C>\n\t<B>A</B>\n\tA\n\t<B>A</B>\n</C>\n')
+ decl + '<C><B>A</B>A<B>A</B></C>\n')
+ # toprettyxml treats whitespace between elements as insignificant
+ self.assertEqual(parseString('<C> <B>A</B> </C>').toprettyxml(),
+ decl + '<C>\n\t \n\t<B>A</B>\n\t \n</C>\n')
def test_toprettyxml_with_adjacent_text_nodes(self):
- # see issue #4147, adjacent text nodes are indented normally
+ # see gh-81623, adjacent text nodes are not separated
dom = Document()
elem = dom.createElement('elem')
elem.appendChild(dom.createTextNode('TEXT'))
elem.appendChild(dom.createTextNode('TEXT'))
dom.appendChild(elem)
decl = '<?xml version="1.0" ?>\n'
- self.assertEqual(dom.toprettyxml(),
- decl + '<elem>\n\tTEXT\n\tTEXT\n</elem>\n')
+ self.assertEqual(dom.toprettyxml(), decl + '<elem>TEXTTEXT</elem>\n')
+
+ def test_toprettyxml_preserve(self):
+ decl = '<?xml version="1.0" ?>\n'
+ # xml:space="preserve" applies to the whole subtree
+ self.assertEqual(
+ parseString('<C xml:space="preserve"><B>A</B><B>A</B></C>'
+ ).toprettyxml(),
+ decl + '<C xml:space="preserve"><B>A</B><B>A</B></C>\n')
+ self.assertEqual(
+ parseString('<C xml:space="preserve"><B><D/></B></C>'
+ ).toprettyxml(),
+ decl + '<C xml:space="preserve"><B><D/></B></C>\n')
+ # other values do not preserve whitespace
+ self.assertEqual(
+ parseString('<C xml:space="default"><B>A</B></C>').toprettyxml(),
+ decl + '<C xml:space="default">\n\t<B>A</B>\n</C>\n')
+
+ def test_toprettyxml_with_non_xml_whitespace(self):
+ # only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
+ decl = '<?xml version="1.0" ?>\n'
+ self.assertEqual(parseString('<C>\xa0<B>A</B></C>').toprettyxml(),
+ decl + '<C>\xa0<B>A</B></C>\n')
+
+ def test_toprettyxml_with_dtd(self):
+ decl = '<?xml version="1.0" ?>\n'
+ # only whitespace in element content is ignorable
+ doctype = ('<!DOCTYPE C [<!ELEMENT C (#PCDATA|B)*>'
+ '<!ELEMENT B (#PCDATA)>]>')
+ self.assertEqual(
+ parseString(doctype + '<C><B>A</B><B>A</B></C>').toprettyxml(),
+ decl + doctype + '\n<C><B>A</B><B>A</B></C>\n')
+ doctype = '<!DOCTYPE C [<!ELEMENT C (B)*><!ELEMENT B (#PCDATA)>]>'
+ self.assertEqual(
+ parseString(doctype + '<C><B>A</B><B>A</B></C>').toprettyxml(),
+ decl + doctype + '\n<C>\n\t<B>A</B>\n\t<B>A</B>\n</C>\n')
+
+ def test_toprettyxml_with_cdata_section(self):
+ decl = '<?xml version="1.0" ?>\n'
+ self.assertEqual(
+ parseString('<C><![CDATA[A]]><B>A</B></C>').toprettyxml(),
+ decl + '<C><![CDATA[A]]><B>A</B></C>\n')
def test_toprettyxml_preserves_content_of_text_node(self):
- # see issue #4147
+ # see gh-48397
for str in ('<B>A</B>', '<A><B>C</B></A>'):
dom = parseString(str)
dom2 = parseString(dom.toprettyxml())
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index f87a47045dd1713..a73db445590032f 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -773,9 +773,10 @@ def test_indent(self):
ET.indent(elem)
self.assertEqual(ET.tostring(elem), b'<html>\n
<body>text</body>\n</html>')
+ # an element with mixed content is not indented
elem = ET.XML("<html><body>text</body>tail</html>")
ET.indent(elem)
- self.assertEqual(ET.tostring(elem), b'<html>\n
<body>text</body>tail</html>')
+ self.assertEqual(ET.tostring(elem),
b'<html><body>text</body>tail</html>')
elem =
ET.XML("<html><body><p>par</p>\n<p>text</p>\t<p><br/></p></body></html>")
ET.indent(elem)
@@ -851,9 +852,39 @@ def test_indent_non_xml_whitespace(self):
ET.indent(elem)
self.assertEqual(
ET.tostring(elem),
- b'<html> <body>\n <p>text</p> </body>\n</html>'
+ b'<html> <body><p>text</p> </body></html>'
)
+ def test_indent_preserve(self):
+ # xml:space="preserve" applies to the whole subtree
+ elem = ET.XML('<html xml:space="preserve"> <body><p>text</p></body>
</html>')
+ ET.indent(elem)
+ self.assertEqual(
+ ET.tostring(elem),
+ b'<html xml:space="preserve"> <body><p>text</p></body> </html>'
+ )
+ # other values do not preserve whitespace
+ elem = ET.XML('<html
xml:space="default"><body><p>text</p></body></html>')
+ ET.indent(elem)
+ self.assertEqual(
+ ET.tostring(elem),
+ b'<html xml:space="default">\n'
+ b' <body>\n'
+ b' <p>text</p>\n'
+ b' </body>\n'
+ b'</html>'
+ )
+
+ def test_indent_mixed_content(self):
+ # whitespace in an element which contains text is significant
+ elem = ET.XML('<p>hello <b>x</b> <i>y</i></p>')
+ ET.indent(elem)
+ self.assertEqual(ET.tostring(elem), b'<p>hello <b>x</b> <i>y</i></p>')
+ # the subtree of such element is not indented either
+ elem = ET.XML('<p>hello <b><i>y</i></b></p>')
+ ET.indent(elem)
+ self.assertEqual(ET.tostring(elem), b'<p>hello <b><i>y</i></b></p>')
+
def test_indent_level(self):
elem =
ET.XML("<html><body><p>pre<br/>post</p><p>text</p></body></html>")
with self.assertRaises(ValueError):
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index 7639fa14c5050fb..93c2e0638493e33 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -940,6 +940,10 @@ def writexml(self, writer, indent="", addindent="",
newl=""):
self.childNodes[0].nodeType in (
Node.TEXT_NODE, Node.CDATA_SECTION_NODE)):
self.childNodes[0].writexml(writer, '', '', '')
+ elif self._preserves_whitespace():
+ # Adding whitespace here would change the content.
+ for node in self.childNodes:
+ node.writexml(writer, '', '', '')
else:
writer.write(newl)
for node in self.childNodes:
@@ -949,6 +953,25 @@ def writexml(self, writer, indent="", addindent="",
newl=""):
else:
writer.write("/>%s"%(newl))
+ def _preserves_whitespace(self):
+ """Returns true iff whitespace in the content is significant.
+
+ This is the case if the element is marked with xml:space="preserve",
+ if the DTD declares that its content model is not element content,
+ or, in absence of such declaration, if it contains text.
+ """
+ if self.getAttribute("xml:space") == "preserve":
+ return True
+ doc = self.ownerDocument
+ info = doc and doc._get_elem_info(self)
+ if info is not None:
+ # Only whitespace in element content is ignorable
+ # (see XML 1.0, 3.2.1).
+ return not info.isElementContent()
+ return any(node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE)
+ and node.data.strip(_XML_WHITESPACE)
+ for node in self.childNodes)
+
def _get_attributes(self):
self._ensure_attributes()
return NamedNodeMap(self._attrs, self._attrsNS, self)
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index 3b4bfa3bd483c2d..65c8e6e7461f1c2 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -104,6 +104,9 @@
# The white space characters of the XML specification (see XML 1.0, 2.3).
_XML_WHITESPACE = " \t\r\n"
+# The xml:space attribute (see XML 1.0, 2.10).
+_XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space"
+
class ParseError(SyntaxError):
"""An error when parsing an XML document.
@@ -1196,7 +1199,20 @@ def indent(tree, space=" ", level=0):
# Reduce the memory consumption by reusing indentation strings.
indentations = ["\n" + level * space]
+ def _preserves_whitespace(elem):
+ # True iff whitespace in the content of the element is significant.
+ if elem.get(_XML_SPACE) == "preserve":
+ return True
+ if elem.text and elem.text.strip(_XML_WHITESPACE):
+ return True
+ return any(child.tail and child.tail.strip(_XML_WHITESPACE)
+ for child in elem)
+
def _indent_children(elem, level):
+ if _preserves_whitespace(elem):
+ # Adding whitespace here would change the content.
+ return
+
# Start a new indentation level for the first child.
child_level = level + 1
try:
@@ -1205,18 +1221,15 @@ def _indent_children(elem, level):
child_indentation = indentations[level] + space
indentations.append(child_indentation)
- if not elem.text or not elem.text.strip(_XML_WHITESPACE):
- elem.text = child_indentation
+ elem.text = child_indentation
for child in elem:
if len(child):
_indent_children(child, child_level)
- if not child.tail or not child.tail.strip(_XML_WHITESPACE):
- child.tail = child_indentation
+ child.tail = child_indentation
# Dedent after the last child by overwriting the previous indentation.
- if not child.tail.strip(_XML_WHITESPACE):
- child.tail = indentations[level]
+ child.tail = indentations[level]
_indent_children(tree, 0)
diff --git
a/Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst
b/Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst
new file mode 100644
index 000000000000000..bdc02a4c55ee03b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst
@@ -0,0 +1,6 @@
+:meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` and
+:func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` no longer
+add whitespace inside an element which is marked with ``xml:space="preserve"``
+or which contains text (:meth:`!toprettyxml` also takes into account the
+content model declared in the DTD). Previously such indentation changed the
+content of the element.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]