Skip to content

Commit 9c9dda6

Browse files
[3.12] gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146) (#142211)
* gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146) * gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) (cherry picked from commit 1cc7551) (cherry picked from commit 08d8e18) (cherry picked from commit 8d2d7bb) Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> Co-authored-by: Seth Michael Larson <seth@python.org> Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent 0e4cd89 commit 9c9dda6

File tree

3 files changed

+41
-9
lines changed

3 files changed

+41
-9
lines changed

Lib/test/test_minidom.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import copy
44
import pickle
5+
import time
56
import io
67
from test import support
78
import unittest
89

910
import xml.dom.minidom
1011

11-
from xml.dom.minidom import parse, Attr, Node, Document, parseString
12+
from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString
1213
from xml.dom.minidom import getDOMImplementation
1314
from xml.parsers.expat import ExpatError
1415

@@ -176,6 +177,36 @@ def testAppendChild(self):
176177
self.confirm(dom.documentElement.childNodes[-1].data == "Hello")
177178
dom.unlink()
178179

180+
@support.requires_resource('cpu')
181+
def testAppendChildNoQuadraticComplexity(self):
182+
impl = getDOMImplementation()
183+
184+
newdoc = impl.createDocument(None, "some_tag", None)
185+
top_element = newdoc.documentElement
186+
children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)]
187+
element = top_element
188+
189+
start = time.monotonic()
190+
for child in children:
191+
element.appendChild(child)
192+
element = child
193+
end = time.monotonic()
194+
195+
# This example used to take at least 30 seconds.
196+
# Conservative assertion due to the wide variety of systems and
197+
# build configs timing based tests wind up run under.
198+
# A --with-address-sanitizer --with-pydebug build on a rpi5 still
199+
# completes this loop in <0.5 seconds.
200+
self.assertLess(end - start, 4)
201+
202+
def testSetAttributeNodeWithoutOwnerDocument(self):
203+
# regression test for gh-142754
204+
elem = Element("test")
205+
attr = Attr("id")
206+
attr.value = "test-id"
207+
elem.setAttributeNode(attr)
208+
self.assertEqual(elem.getAttribute("id"), "test-id")
209+
179210
def testAppendChildFragment(self):
180211
dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes()
181212
dom.documentElement.appendChild(frag)

Lib/xml/dom/minidom.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,13 +292,6 @@ def _append_child(self, node):
292292
childNodes.append(node)
293293
node.parentNode = self
294294

295-
def _in_document(node):
296-
# return True iff node is part of a document tree
297-
while node is not None:
298-
if node.nodeType == Node.DOCUMENT_NODE:
299-
return True
300-
node = node.parentNode
301-
return False
302295

303296
def _write_data(writer, data):
304297
"Writes datachars to writer."
@@ -355,6 +348,7 @@ class Attr(Node):
355348
def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None,
356349
prefix=None):
357350
self.ownerElement = None
351+
self.ownerDocument = None
358352
self._name = qName
359353
self.namespaceURI = namespaceURI
360354
self._prefix = prefix
@@ -680,6 +674,7 @@ class Element(Node):
680674

681675
def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None,
682676
localName=None):
677+
self.ownerDocument = None
683678
self.parentNode = None
684679
self.tagName = self.nodeName = tagName
685680
self.prefix = prefix
@@ -1539,7 +1534,7 @@ def _clear_id_cache(node):
15391534
if node.nodeType == Node.DOCUMENT_NODE:
15401535
node._id_cache.clear()
15411536
node._id_search_stack = None
1542-
elif _in_document(node):
1537+
elif node.ownerDocument:
15431538
node.ownerDocument._id_cache.clear()
15441539
node.ownerDocument._id_search_stack= None
15451540

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. In order
2+
to do this without breaking existing users, we also add the *ownerDocument*
3+
attribute to :mod:`xml.dom.minidom` elements and attributes created by directly
4+
instantiating the ``Element`` or ``Attr`` class. Note that this way of creating
5+
nodes is not supported; creator functions like
6+
:py:meth:`xml.dom.Document.documentElement` should be used instead.

0 commit comments

Comments
 (0)