Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Lib/test/test_minidom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,21 @@ def testNormalizeDeleteAndCombine(self):
, "testNormalizeDeleteAndCombine -- result")
doc.unlink()

def testNormalizeManyTextNodes(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't assert anything, so it doesn't really verify the change. I think it's hard to write a test for this, so it may be fine to just remove it.

I've run the benchmark locally (debug build, one element with n text nodes of 48 chars each):

n before after
8,000 2.8 s 18 ms
16,000 11.3 s 37 ms
32,000 45.6 s 75 ms
64,000 181 s 148 ms

# Ensure that normalize() is fast with many adjacent text nodes.
N = 100_000
doc = parseString("<doc/>")
root = doc.documentElement
for i in range(N):
root.appendChild(doc.createTextNode("x" * 16))
if i % 3 == 0:
root.appendChild(doc.createTextNode(""))
doc.normalize()
self.assertEqual(len(root.childNodes), 1)
self.assertEqual(root.firstChild.data, "x" * 16 * N)
self.assertIsNone(root.firstChild.nextSibling)
doc.unlink()

def testNormalizeRecursion(self):
doc = parseString("<doc>"
"<o>"
Expand Down
13 changes: 12 additions & 1 deletion Lib/xml/dom/minidom.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ def removeChild(self, oldChild):

def normalize(self):
L = []
# (text node, [data, ...]) for each text node that absorbs others.
# Join each run once at the end; concatenating as we go is quadratic.
runs = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can initialize it here, then the None guards at L214 and L230 can be removed:

Suggested change
runs = None
runs = []

for child in self.childNodes:
if child.nodeType == Node.TEXT_NODE:
if not child.data:
Expand All @@ -208,7 +211,12 @@ def normalize(self):
elif L and L[-1].nodeType == child.nodeType:
# collapse text node
node = L[-1]
node.data = node.data + child.data
if runs is None:
runs = []
if runs and runs[-1][0] is node:
runs[-1][1].append(child.data)
else:
runs.append((node, [node.data, child.data]))
node.nextSibling = child.nextSibling
if child.nextSibling:
child.nextSibling.previousSibling = node
Expand All @@ -219,6 +227,9 @@ def normalize(self):
L.append(child)
if child.nodeType == Node.ELEMENT_NODE:
child.normalize()
if runs is not None:
for node, data in runs:
node.data = ''.join(data)
self.childNodes[:] = L

def cloneNode(self, deep):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:meth:`xml.dom.minidom.Node.normalize` now merges runs of adjacent text
nodes in linear time instead of quadratic time.
Loading