Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3308360

Browse files
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.
1 parent bbdf31d commit 3308360

8 files changed

Lines changed: 168 additions & 16 deletions

File tree

Doc/library/xml.dom.minidom.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,23 @@ module documentation. This section lists the differences between the API and
187187

188188
The *standalone* argument behaves exactly as in :meth:`writexml`.
189189

190+
No indentation is added inside an element
191+
which is marked with ``xml:space="preserve"``,
192+
which is declared in the DTD as not having element content,
193+
or, in absence of such declaration, which contains text,
194+
because this would change its content.
195+
190196
.. versionchanged:: 3.8
191197
The :meth:`toprettyxml` method now preserves the attribute order specified
192198
by the user.
193199

194200
.. versionchanged:: 3.9
195201
The *standalone* parameter was added.
196202

203+
.. versionchanged:: next
204+
Whitespace is no longer added inside an element with mixed content
205+
or marked with ``xml:space="preserve"``.
206+
197207
.. _dom-example:
198208

199209
DOM Example

Doc/library/xml.etree.elementtree.rst

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

634+
No whitespace is added inside an element
635+
which is marked with ``xml:space="preserve"``
636+
or which contains text, because this would change its content.
637+
634638
.. versionadded:: 3.9
635639

640+
.. versionchanged:: next
641+
Whitespace is no longer added inside an element with mixed content
642+
or marked with ``xml:space="preserve"``.
643+
636644

637645
.. function:: iselement(element)
638646

Doc/whatsnew/3.16.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,14 @@ xml
702702
and :meth:`!Document.createEntityReference`.
703703
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
704704

705+
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
706+
and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
707+
no longer add whitespace inside an element
708+
which is marked with ``xml:space="preserve"`` or which contains text.
709+
:meth:`!toprettyxml` also takes into account
710+
the content model declared in the DTD.
711+
(Contributed by Serhiy Storchaka in :gh:`81623`.)
712+
705713
* Add :meth:`!GetSpecifiedAttributeCount` method
706714
to the :mod:`XML parser <xml.parsers.expat>` objects.
707715
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.
947955
Attributes defaulted in the DTD are no longer omitted when parsing.
948956
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
949957

958+
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
959+
and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
960+
no longer add whitespace inside an element
961+
which is marked with ``xml:space="preserve"`` or which contains text,
962+
because this changed the content of the element.
963+
:meth:`!toprettyxml` also takes into account
964+
the content model declared in the DTD.
965+
(Contributed by Serhiy Storchaka in :gh:`81623`.)
966+
950967
* On Windows, seeking a pipe now fails instead of silently appearing to
951968
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
952969
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,

Lib/test/test_minidom.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -617,32 +617,76 @@ def testAltNewline(self):
617617
self.assertEqual(domstr, str.replace("\n", "\r\n"))
618618

619619
def test_toprettyxml_with_text_nodes(self):
620-
# see issue #4147, text nodes are not indented
620+
# see gh-48397 and gh-81623,
621+
# the content of an element with text is not changed
621622
decl = '<?xml version="1.0" ?>\n'
622623
self.assertEqual(parseString('<B>A</B>').toprettyxml(),
623624
decl + '<B>A</B>\n')
624625
self.assertEqual(parseString('<C>A<B>A</B></C>').toprettyxml(),
625-
decl + '<C>\n\tA\n\t<B>A</B>\n</C>\n')
626+
decl + '<C>A<B>A</B></C>\n')
626627
self.assertEqual(parseString('<C><B>A</B>A</C>').toprettyxml(),
627-
decl + '<C>\n\t<B>A</B>\n\tA\n</C>\n')
628+
decl + '<C><B>A</B>A</C>\n')
628629
self.assertEqual(parseString('<C><B>A</B><B>A</B></C>').toprettyxml(),
629630
decl + '<C>\n\t<B>A</B>\n\t<B>A</B>\n</C>\n')
630631
self.assertEqual(parseString('<C><B>A</B>A<B>A</B></C>').toprettyxml(),
631-
decl + '<C>\n\t<B>A</B>\n\tA\n\t<B>A</B>\n</C>\n')
632+
decl + '<C><B>A</B>A<B>A</B></C>\n')
633+
# toprettyxml treats whitespace between elements as insignificant
634+
self.assertEqual(parseString('<C> <B>A</B> </C>').toprettyxml(),
635+
decl + '<C>\n\t \n\t<B>A</B>\n\t \n</C>\n')
632636

633637
def test_toprettyxml_with_adjacent_text_nodes(self):
634-
# see issue #4147, adjacent text nodes are indented normally
638+
# see gh-81623, adjacent text nodes are not separated
635639
dom = Document()
636640
elem = dom.createElement('elem')
637641
elem.appendChild(dom.createTextNode('TEXT'))
638642
elem.appendChild(dom.createTextNode('TEXT'))
639643
dom.appendChild(elem)
640644
decl = '<?xml version="1.0" ?>\n'
641-
self.assertEqual(dom.toprettyxml(),
642-
decl + '<elem>\n\tTEXT\n\tTEXT\n</elem>\n')
645+
self.assertEqual(dom.toprettyxml(), decl + '<elem>TEXTTEXT</elem>\n')
646+
647+
def test_toprettyxml_preserve(self):
648+
decl = '<?xml version="1.0" ?>\n'
649+
# xml:space="preserve" applies to the whole subtree
650+
self.assertEqual(
651+
parseString('<C xml:space="preserve"><B>A</B><B>A</B></C>'
652+
).toprettyxml(),
653+
decl + '<C xml:space="preserve"><B>A</B><B>A</B></C>\n')
654+
self.assertEqual(
655+
parseString('<C xml:space="preserve"><B><D/></B></C>'
656+
).toprettyxml(),
657+
decl + '<C xml:space="preserve"><B><D/></B></C>\n')
658+
# other values do not preserve whitespace
659+
self.assertEqual(
660+
parseString('<C xml:space="default"><B>A</B></C>').toprettyxml(),
661+
decl + '<C xml:space="default">\n\t<B>A</B>\n</C>\n')
662+
663+
def test_toprettyxml_with_non_xml_whitespace(self):
664+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
665+
decl = '<?xml version="1.0" ?>\n'
666+
self.assertEqual(parseString('<C>\xa0<B>A</B></C>').toprettyxml(),
667+
decl + '<C>\xa0<B>A</B></C>\n')
668+
669+
def test_toprettyxml_with_dtd(self):
670+
decl = '<?xml version="1.0" ?>\n'
671+
# only whitespace in element content is ignorable
672+
doctype = ('<!DOCTYPE C [<!ELEMENT C (#PCDATA|B)*>'
673+
'<!ELEMENT B (#PCDATA)>]>')
674+
self.assertEqual(
675+
parseString(doctype + '<C><B>A</B><B>A</B></C>').toprettyxml(),
676+
decl + doctype + '\n<C><B>A</B><B>A</B></C>\n')
677+
doctype = '<!DOCTYPE C [<!ELEMENT C (B)*><!ELEMENT B (#PCDATA)>]>'
678+
self.assertEqual(
679+
parseString(doctype + '<C><B>A</B><B>A</B></C>').toprettyxml(),
680+
decl + doctype + '\n<C>\n\t<B>A</B>\n\t<B>A</B>\n</C>\n')
681+
682+
def test_toprettyxml_with_cdata_section(self):
683+
decl = '<?xml version="1.0" ?>\n'
684+
self.assertEqual(
685+
parseString('<C><![CDATA[A]]><B>A</B></C>').toprettyxml(),
686+
decl + '<C><![CDATA[A]]><B>A</B></C>\n')
643687

644688
def test_toprettyxml_preserves_content_of_text_node(self):
645-
# see issue #4147
689+
# see gh-48397
646690
for str in ('<B>A</B>', '<A><B>C</B></A>'):
647691
dom = parseString(str)
648692
dom2 = parseString(dom.toprettyxml())

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/dom/minidom.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,10 @@ def writexml(self, writer, indent="", addindent="", newl=""):
940940
self.childNodes[0].nodeType in (
941941
Node.TEXT_NODE, Node.CDATA_SECTION_NODE)):
942942
self.childNodes[0].writexml(writer, '', '', '')
943+
elif self._preserves_whitespace():
944+
# Adding whitespace here would change the content.
945+
for node in self.childNodes:
946+
node.writexml(writer, '', '', '')
943947
else:
944948
writer.write(newl)
945949
for node in self.childNodes:
@@ -949,6 +953,25 @@ def writexml(self, writer, indent="", addindent="", newl=""):
949953
else:
950954
writer.write("/>%s"%(newl))
951955

956+
def _preserves_whitespace(self):
957+
"""Returns true iff whitespace in the content is significant.
958+
959+
This is the case if the element is marked with xml:space="preserve",
960+
if the DTD declares that its content model is not element content,
961+
or, in absence of such declaration, if it contains text.
962+
"""
963+
if self.getAttribute("xml:space") == "preserve":
964+
return True
965+
doc = self.ownerDocument
966+
info = doc and doc._get_elem_info(self)
967+
if info is not None:
968+
# Only whitespace in element content is ignorable
969+
# (see XML 1.0, 3.2.1).
970+
return not info.isElementContent()
971+
return any(node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE)
972+
and node.data.strip(_XML_WHITESPACE)
973+
for node in self.childNodes)
974+
952975
def _get_attributes(self):
953976
self._ensure_attributes()
954977
return NamedNodeMap(self._attrs, self._attrsNS, self)

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
@@ -1196,7 +1199,20 @@ def indent(tree, space=" ", level=0):
11961199
# Reduce the memory consumption by reusing indentation strings.
11971200
indentations = ["\n" + level * space]
11981201

1202+
def _preserves_whitespace(elem):
1203+
# True iff whitespace in the content of the element is significant.
1204+
if elem.get(_XML_SPACE) == "preserve":
1205+
return True
1206+
if elem.text and elem.text.strip(_XML_WHITESPACE):
1207+
return True
1208+
return any(child.tail and child.tail.strip(_XML_WHITESPACE)
1209+
for child in elem)
1210+
11991211
def _indent_children(elem, level):
1212+
if _preserves_whitespace(elem):
1213+
# Adding whitespace here would change the content.
1214+
return
1215+
12001216
# Start a new indentation level for the first child.
12011217
child_level = level + 1
12021218
try:
@@ -1205,18 +1221,15 @@ def _indent_children(elem, level):
12051221
child_indentation = indentations[level] + space
12061222
indentations.append(child_indentation)
12071223

1208-
if not elem.text or not elem.text.strip(_XML_WHITESPACE):
1209-
elem.text = child_indentation
1224+
elem.text = child_indentation
12101225

12111226
for child in elem:
12121227
if len(child):
12131228
_indent_children(child, child_level)
1214-
if not child.tail or not child.tail.strip(_XML_WHITESPACE):
1215-
child.tail = child_indentation
1229+
child.tail = child_indentation
12161230

12171231
# Dedent after the last child by overwriting the previous indentation.
1218-
if not child.tail.strip(_XML_WHITESPACE):
1219-
child.tail = indentations[level]
1232+
child.tail = indentations[level]
12201233

12211234
_indent_children(tree, 0)
12221235

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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)