From 54a3ade9d76ad6cf935e5192fe523e6e720e3030 Mon Sep 17 00:00:00 2001 From: sricursion Date: Wed, 7 Oct 2026 12:24:45 +0530 Subject: [PATCH 1/2] gh-158978: Fix cloning of DOM Level 1 nodes in xml.dom.minidom cloneNode() and importNode() created the clones with createElementNS(), setAttributeNS() and createAttributeNS(), which now reject a prefixed name in no namespace. Create the Element and Attr nodes directly instead. --- Lib/test/test_minidom.py | 87 ++++++++++++++++++++++++++++++++++++++++ Lib/xml/dom/minidom.py | 17 +++++--- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 446bbe096bd19dc..c7bd81e590fafbd 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -6,6 +6,7 @@ from test import support import unittest +import xml.dom.expatbuilder import xml.dom.minidom from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString @@ -1277,6 +1278,65 @@ def testCloneElementDeep(self): self.assertTrue(clone.toxml(), '') dom.unlink() + def create_dom1_element(self): + # DOM Level 1 nodes have no namespace, but their names can contain + # a colon. + doc = Document() + root = doc.appendChild(doc.createElement("root")) + root.setAttribute("xmlns", "http://xml.python.org/default") + root.setAttribute("xmlns:xsi", "http://xml.python.org/xsi") + root.setAttribute("xsi:type", "t") + root.appendChild(doc.createElement("svg:rect")) + return doc, root + + def check_dom1_names(self, node, nodeName, prefix, localName): + self.assertEqual(node.nodeName, nodeName) + self.assertEqual(node.prefix, prefix) + self.assertEqual(node.localName, localName) + self.assertIsNone(node.namespaceURI) + + def check_dom1_element(self, elem, doc): + self.assertIs(elem.ownerDocument, doc) + self.check_dom1_names(elem, "root", None, "root") + self.assertEqual(len(elem.attributes), 3) + for name, prefix, localName, value in [ + ("xmlns", None, "xmlns", "http://xml.python.org/default"), + ("xmlns:xsi", "xmlns", "xsi", "http://xml.python.org/xsi"), + ("xsi:type", "xsi", "type", "t"), + ]: + with self.subTest(name=name): + attr = elem.getAttributeNode(name) + self.check_dom1_names(attr, name, prefix, localName) + self.assertEqual(attr.value, value) + self.assertIs(attr.ownerElement, elem) + self.assertIs(attr.ownerDocument, doc) + self.assertEqual(len(elem.childNodes), 1) + self.check_dom1_names(elem.firstChild, "svg:rect", "svg", "rect") + self.assertIs(elem.firstChild.ownerDocument, doc) + + def testCloneElementDOM1Names(self): + doc, root = self.create_dom1_element() + clone = root.cloneNode(True) + self.assertIsNot(clone, root) + self.assertIsNone(clone.parentNode) + self.check_dom1_element(clone, doc) + doc.unlink() + + def testCloneElementNoNamespaces(self): + doc = xml.dom.expatbuilder.parseString( + "", + namespaces=False) + clone = doc.documentElement.cloneNode(True) + self.check_dom1_names(clone, "a:b", "a", "b") + self.assertEqual(clone.getAttribute("xmlns:a"), + "http://xml.python.org/a") + self.check_dom1_names(clone.getAttributeNode("xmlns:a"), + "xmlns:a", "xmlns", "a") + self.assertEqual(clone.getAttribute("a:x"), "1") + self.check_dom1_names(clone.getAttributeNode("a:x"), "a:x", "a", "x") + self.check_dom1_names(clone.firstChild, "a:c", "a", "c") + doc.unlink() + def testCloneDocumentShallow(self): doc = parseString("\n" "" @@ -1395,6 +1455,15 @@ def testImportDocumentTypeDeep(self): self.assertRaises(xml.dom.NotSupportedErr, target.importNode, src.doctype, 1) + def testImportElementDOM1Names(self): + doc1, root = self.create_dom1_element() + doc2 = Document() + imported = doc2.importNode(root, True) + self.assertIsNone(imported.parentNode) + self.check_dom1_element(imported, doc2) + doc1.unlink() + doc2.unlink() + # Testing attribute clones uses a helper, and should always be deep, # even if the argument to cloneNode is false. def check_clone_attribute(self, deep, testName): @@ -1417,6 +1486,24 @@ def testCloneAttributeShallow(self): def testCloneAttributeDeep(self): self.check_clone_attribute(1, "testCloneAttributeDeep") + def testCloneAttributeDOM1Names(self): + doc1 = Document() + attr = doc1.createAttribute("a:b") + attr.value = "v" + doc2 = Document() + for method, clone, owner in [ + ("cloneNode", attr.cloneNode(True), doc1), + ("importNode", doc2.importNode(attr, True), doc2), + ]: + with self.subTest(method=method): + self.check_dom1_names(clone, "a:b", "a", "b") + self.assertEqual(clone.value, "v") + self.assertTrue(clone.specified) + self.assertIsNone(clone.ownerElement) + self.assertIs(clone.ownerDocument, owner) + doc1.unlink() + doc2.unlink() + def check_clone_pi(self, deep, testName): doc = parseString("") pi = doc.firstChild diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 7cb652a323dcc22..0eedc678d7aac25 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -2169,11 +2169,15 @@ def _clone_node(node, deep, newOwnerDocument): else: operation = xml.dom.UserDataHandler.NODE_IMPORTED if node.nodeType == Node.ELEMENT_NODE: - clone = newOwnerDocument.createElementNS(node.namespaceURI, - node.nodeName) + prefix, localName = _nssplit(node.nodeName) + clone = Element(node.nodeName, node.namespaceURI, prefix) + clone.ownerDocument = newOwnerDocument for attr in node.attributes.values(): - clone.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value) - a = clone.getAttributeNodeNS(attr.namespaceURI, attr.localName) + prefix, localName = _nssplit(attr.nodeName) + a = Attr(attr.nodeName, attr.namespaceURI, localName, prefix) + a.value = attr.value + a.ownerDocument = newOwnerDocument + clone.setAttributeNode(a) a.specified = attr.specified if deep: @@ -2200,8 +2204,9 @@ def _clone_node(node, deep, newOwnerDocument): elif node.nodeType == Node.ENTITY_REFERENCE_NODE: clone = newOwnerDocument.createEntityReference(node.nodeName) elif node.nodeType == Node.ATTRIBUTE_NODE: - clone = newOwnerDocument.createAttributeNS(node.namespaceURI, - node.nodeName) + prefix, localName = _nssplit(node.nodeName) + clone = Attr(node.nodeName, node.namespaceURI, localName, prefix) + clone.ownerDocument = newOwnerDocument clone.specified = True clone.value = node.value elif node.nodeType == Node.DOCUMENT_TYPE_NODE: From 8d816b9267cebc21f2a361d8af7b35f12a71bc34 Mon Sep 17 00:00:00 2001 From: sricursion Date: Thu, 12 Nov 2026 07:50:23 +0530 Subject: [PATCH 2/2] Use the document factory methods when cloning nodes Clone nodes without a namespace with createElement(), setAttribute() and createAttribute(), and namespaced nodes with the NS variants, so that a Document subclass can create its own node classes and the clone keeps the prefix of the original. The tests now compare the names of the clone with those of the original. --- Lib/test/test_minidom.py | 67 ++++++++++++++++------------------------ Lib/xml/dom/minidom.py | 29 ++++++++++------- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index c7bd81e590fafbd..c542eb10b2784d9 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -1286,55 +1286,42 @@ def create_dom1_element(self): root.setAttribute("xmlns", "http://xml.python.org/default") root.setAttribute("xmlns:xsi", "http://xml.python.org/xsi") root.setAttribute("xsi:type", "t") + root.setAttributeNS("http://xml.python.org/ns", "p:a", "v") root.appendChild(doc.createElement("svg:rect")) + root.appendChild( + doc.createElementNS("http://xml.python.org/ns", "p:b")) return doc, root - def check_dom1_names(self, node, nodeName, prefix, localName): - self.assertEqual(node.nodeName, nodeName) - self.assertEqual(node.prefix, prefix) - self.assertEqual(node.localName, localName) - self.assertIsNone(node.namespaceURI) - - def check_dom1_element(self, elem, doc): - self.assertIs(elem.ownerDocument, doc) - self.check_dom1_names(elem, "root", None, "root") - self.assertEqual(len(elem.attributes), 3) - for name, prefix, localName, value in [ - ("xmlns", None, "xmlns", "http://xml.python.org/default"), - ("xmlns:xsi", "xmlns", "xsi", "http://xml.python.org/xsi"), - ("xsi:type", "xsi", "type", "t"), - ]: - with self.subTest(name=name): - attr = elem.getAttributeNode(name) - self.check_dom1_names(attr, name, prefix, localName) - self.assertEqual(attr.value, value) - self.assertIs(attr.ownerElement, elem) - self.assertIs(attr.ownerDocument, doc) - self.assertEqual(len(elem.childNodes), 1) - self.check_dom1_names(elem.firstChild, "svg:rect", "svg", "rect") - self.assertIs(elem.firstChild.ownerDocument, doc) + def check_clone_names(self, clone, node): + for name in ("nodeName", "prefix", "localName", "namespaceURI"): + self.assertEqual(getattr(clone, name), getattr(node, name), name) + + def check_clone_element(self, clone, elem, doc): + self.assertIs(clone.ownerDocument, doc) + self.assertIsNone(clone.parentNode) + self.check_clone_names(clone, elem) + self.assertEqual(len(clone.attributes), len(elem.attributes)) + for attr in elem.attributes.values(): + with self.subTest(attr=attr.name): + a = clone.getAttributeNode(attr.name) + self.check_clone_names(a, attr) + self.assertEqual(a.value, attr.value) + self.assertIs(a.ownerElement, clone) + self.assertEqual(len(clone.childNodes), len(elem.childNodes)) + for c, child in zip(clone.childNodes, elem.childNodes): + self.check_clone_names(c, child) def testCloneElementDOM1Names(self): doc, root = self.create_dom1_element() - clone = root.cloneNode(True) - self.assertIsNot(clone, root) - self.assertIsNone(clone.parentNode) - self.check_dom1_element(clone, doc) + self.check_clone_element(root.cloneNode(True), root, doc) doc.unlink() def testCloneElementNoNamespaces(self): doc = xml.dom.expatbuilder.parseString( "", namespaces=False) - clone = doc.documentElement.cloneNode(True) - self.check_dom1_names(clone, "a:b", "a", "b") - self.assertEqual(clone.getAttribute("xmlns:a"), - "http://xml.python.org/a") - self.check_dom1_names(clone.getAttributeNode("xmlns:a"), - "xmlns:a", "xmlns", "a") - self.assertEqual(clone.getAttribute("a:x"), "1") - self.check_dom1_names(clone.getAttributeNode("a:x"), "a:x", "a", "x") - self.check_dom1_names(clone.firstChild, "a:c", "a", "c") + root = doc.documentElement + self.check_clone_element(root.cloneNode(True), root, doc) doc.unlink() def testCloneDocumentShallow(self): @@ -1458,9 +1445,7 @@ def testImportDocumentTypeDeep(self): def testImportElementDOM1Names(self): doc1, root = self.create_dom1_element() doc2 = Document() - imported = doc2.importNode(root, True) - self.assertIsNone(imported.parentNode) - self.check_dom1_element(imported, doc2) + self.check_clone_element(doc2.importNode(root, True), root, doc2) doc1.unlink() doc2.unlink() @@ -1496,7 +1481,7 @@ def testCloneAttributeDOM1Names(self): ("importNode", doc2.importNode(attr, True), doc2), ]: with self.subTest(method=method): - self.check_dom1_names(clone, "a:b", "a", "b") + self.check_clone_names(clone, attr) self.assertEqual(clone.value, "v") self.assertTrue(clone.specified) self.assertIsNone(clone.ownerElement) diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 0eedc678d7aac25..f4d544e08bd3e94 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -2169,15 +2169,20 @@ def _clone_node(node, deep, newOwnerDocument): else: operation = xml.dom.UserDataHandler.NODE_IMPORTED if node.nodeType == Node.ELEMENT_NODE: - prefix, localName = _nssplit(node.nodeName) - clone = Element(node.nodeName, node.namespaceURI, prefix) - clone.ownerDocument = newOwnerDocument + if node.namespaceURI is None: + clone = newOwnerDocument.createElement(node.nodeName) + else: + clone = newOwnerDocument.createElementNS(node.namespaceURI, + node.nodeName) for attr in node.attributes.values(): - prefix, localName = _nssplit(attr.nodeName) - a = Attr(attr.nodeName, attr.namespaceURI, localName, prefix) - a.value = attr.value - a.ownerDocument = newOwnerDocument - clone.setAttributeNode(a) + if attr.namespaceURI is None: + clone.setAttribute(attr.nodeName, attr.value) + a = clone.getAttributeNode(attr.nodeName) + else: + clone.setAttributeNS(attr.namespaceURI, attr.nodeName, + attr.value) + a = clone.getAttributeNodeNS(attr.namespaceURI, + attr.localName) a.specified = attr.specified if deep: @@ -2204,9 +2209,11 @@ def _clone_node(node, deep, newOwnerDocument): elif node.nodeType == Node.ENTITY_REFERENCE_NODE: clone = newOwnerDocument.createEntityReference(node.nodeName) elif node.nodeType == Node.ATTRIBUTE_NODE: - prefix, localName = _nssplit(node.nodeName) - clone = Attr(node.nodeName, node.namespaceURI, localName, prefix) - clone.ownerDocument = newOwnerDocument + if node.namespaceURI is None: + clone = newOwnerDocument.createAttribute(node.nodeName) + else: + clone = newOwnerDocument.createAttributeNS(node.namespaceURI, + node.nodeName) clone.specified = True clone.value = node.value elif node.nodeType == Node.DOCUMENT_TYPE_NODE: