Skip to content

Commit 159e84a

Browse files
gh-81623: Do not add whitespace to significant content in indent()
ElementTree.indent() only avoided overwriting text and tails which are not white space, but in an element which contains text the white space between them is significant too. xml:space="preserve" is now honored as well.
1 parent 37616fa commit 159e84a

5 files changed

Lines changed: 76 additions & 21 deletions

File tree

Doc/library/xml.etree.elementtree.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,16 @@ Functions
603603
characters by default. For indenting partial subtrees inside of an
604604
already indented tree, pass the initial indentation level as *level*.
605605

606+
No whitespace is added inside an element
607+
which is marked with ``xml:space="preserve"``
608+
or which contains text, because this would change its content.
609+
606610
.. versionadded:: 3.9
607611

612+
.. versionchanged:: next
613+
Whitespace is no longer added inside an element with mixed content
614+
or marked with ``xml:space="preserve"``.
615+
608616

609617
.. function:: iselement(element)
610618

Doc/whatsnew/3.16.rst

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -643,10 +643,11 @@ xml
643643
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
644644

645645
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
646-
no longer adds whitespace inside an element
647-
which is marked with ``xml:space="preserve"``,
648-
which is declared in the DTD as not having element content,
649-
or, in absence of such declaration, which contains text.
646+
and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
647+
no longer add whitespace inside an element
648+
which is marked with ``xml:space="preserve"`` or which contains text.
649+
:meth:`!toprettyxml` also takes into account
650+
the content model declared in the DTD.
650651
(Contributed by Serhiy Storchaka in :gh:`81623`.)
651652

652653
* Add :meth:`!GetSpecifiedAttributeCount` method
@@ -889,11 +890,12 @@ that may require changes to your code.
889890
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
890891

891892
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
892-
no longer adds whitespace inside an element
893-
which is marked with ``xml:space="preserve"``,
894-
which is declared in the DTD as not having element content,
895-
or, in absence of such declaration, which contains text,
893+
and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
894+
no longer add whitespace inside an element
895+
which is marked with ``xml:space="preserve"`` or which contains text,
896896
because this changed the content of the element.
897+
:meth:`!toprettyxml` also takes into account
898+
the content model declared in the DTD.
897899
(Contributed by Serhiy Storchaka in :gh:`81623`.)
898900

899901
* On Windows, seeking a pipe now fails instead of silently appearing to

Lib/test/test_xml_etree.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,9 +773,10 @@ def test_indent(self):
773773
ET.indent(elem)
774774
self.assertEqual(ET.tostring(elem), b'<html>\n <body>text</body>\n</html>')
775775

776+
# an element with mixed content is not indented
776777
elem = ET.XML("<html><body>text</body>tail</html>")
777778
ET.indent(elem)
778-
self.assertEqual(ET.tostring(elem), b'<html>\n <body>text</body>tail</html>')
779+
self.assertEqual(ET.tostring(elem), b'<html><body>text</body>tail</html>')
779780

780781
elem = ET.XML("<html><body><p>par</p>\n<p>text</p>\t<p><br/></p></body></html>")
781782
ET.indent(elem)
@@ -851,9 +852,39 @@ def test_indent_non_xml_whitespace(self):
851852
ET.indent(elem)
852853
self.assertEqual(
853854
ET.tostring(elem),
854-
b'<html>&#160;<body>\n <p>text</p>&#160;</body>\n</html>'
855+
b'<html>&#160;<body><p>text</p>&#160;</body></html>'
855856
)
856857

858+
def test_indent_preserve(self):
859+
# xml:space="preserve" applies to the whole subtree
860+
elem = ET.XML('<html xml:space="preserve"> <body><p>text</p></body> </html>')
861+
ET.indent(elem)
862+
self.assertEqual(
863+
ET.tostring(elem),
864+
b'<html xml:space="preserve"> <body><p>text</p></body> </html>'
865+
)
866+
# other values do not preserve whitespace
867+
elem = ET.XML('<html xml:space="default"><body><p>text</p></body></html>')
868+
ET.indent(elem)
869+
self.assertEqual(
870+
ET.tostring(elem),
871+
b'<html xml:space="default">\n'
872+
b' <body>\n'
873+
b' <p>text</p>\n'
874+
b' </body>\n'
875+
b'</html>'
876+
)
877+
878+
def test_indent_mixed_content(self):
879+
# whitespace in an element which contains text is significant
880+
elem = ET.XML('<p>hello <b>x</b> <i>y</i></p>')
881+
ET.indent(elem)
882+
self.assertEqual(ET.tostring(elem), b'<p>hello <b>x</b> <i>y</i></p>')
883+
# the subtree of such element is not indented either
884+
elem = ET.XML('<p>hello <b><i>y</i></b></p>')
885+
ET.indent(elem)
886+
self.assertEqual(ET.tostring(elem), b'<p>hello <b><i>y</i></b></p>')
887+
857888
def test_indent_level(self):
858889
elem = ET.XML("<html><body><p>pre<br/>post</p><p>text</p></body></html>")
859890
with self.assertRaises(ValueError):

Lib/xml/etree/ElementTree.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104
# The white space characters of the XML specification (see XML 1.0, 2.3).
105105
_XML_WHITESPACE = " \t\r\n"
106106

107+
# The xml:space attribute (see XML 1.0, 2.10).
108+
_XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space"
109+
107110
class ParseError(SyntaxError):
108111
"""An error when parsing an XML document.
109112
@@ -1184,7 +1187,20 @@ def indent(tree, space=" ", level=0):
11841187
# Reduce the memory consumption by reusing indentation strings.
11851188
indentations = ["\n" + level * space]
11861189

1190+
def _preserves_whitespace(elem):
1191+
# True iff whitespace in the content of the element is significant.
1192+
if elem.get(_XML_SPACE) == "preserve":
1193+
return True
1194+
if elem.text and elem.text.strip(_XML_WHITESPACE):
1195+
return True
1196+
return any(child.tail and child.tail.strip(_XML_WHITESPACE)
1197+
for child in elem)
1198+
11871199
def _indent_children(elem, level):
1200+
if _preserves_whitespace(elem):
1201+
# Adding whitespace here would change the content.
1202+
return
1203+
11881204
# Start a new indentation level for the first child.
11891205
child_level = level + 1
11901206
try:
@@ -1193,18 +1209,15 @@ def _indent_children(elem, level):
11931209
child_indentation = indentations[level] + space
11941210
indentations.append(child_indentation)
11951211

1196-
if not elem.text or not elem.text.strip(_XML_WHITESPACE):
1197-
elem.text = child_indentation
1212+
elem.text = child_indentation
11981213

11991214
for child in elem:
12001215
if len(child):
12011216
_indent_children(child, child_level)
1202-
if not child.tail or not child.tail.strip(_XML_WHITESPACE):
1203-
child.tail = child_indentation
1217+
child.tail = child_indentation
12041218

12051219
# Dedent after the last child by overwriting the previous indentation.
1206-
if not child.tail.strip(_XML_WHITESPACE):
1207-
child.tail = indentations[level]
1220+
child.tail = indentations[level]
12081221

12091222
_indent_children(tree, 0)
12101223

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
:meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` no longer
2-
adds whitespace inside an element which is marked with
3-
``xml:space="preserve"``, which is declared in the DTD as not having element
4-
content, or, in absence of such declaration, which contains text. Previously
5-
such indentation changed the content of the element.
1+
:meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` and
2+
:func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` no longer
3+
add whitespace inside an element which is marked with ``xml:space="preserve"``
4+
or which contains text (:meth:`!toprettyxml` also takes into account the
5+
content model declared in the DTD). Previously such indentation changed the
6+
content of the element.

0 commit comments

Comments
 (0)