From feabab3a5ef0e4a4c1400d2b0b9aacd85af7dd0c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 12 Aug 2026 21:56:59 +0300 Subject: [PATCH 1/2] gh-44871: Add pyexpat GetSpecifiedAttributeCount() It tells how many of the attributes reported to StartElementHandler were given in the start tag rather than defaulted from the DTD. --- Doc/library/pyexpat.rst | 14 ++++++++++ Doc/whatsnew/3.16.rst | 6 +++++ ...6-08-12-21-34-12.gh-issue-44871.Rt7Nv2.rst | 4 +++ Modules/clinic/pyexpat.c.h | 26 ++++++++++++++++++- Modules/pyexpat.c | 20 ++++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Rt7Nv2.rst diff --git a/Doc/library/pyexpat.rst b/Doc/library/pyexpat.rst index c88411ce0b7b91..abd68c5006bd47 100644 --- a/Doc/library/pyexpat.rst +++ b/Doc/library/pyexpat.rst @@ -172,6 +172,20 @@ XMLParser Objects or ``None`` if :meth:`SetBase` hasn't been called. +.. method:: xmlparser.GetSpecifiedAttributeCount() + + Return the index just past the attributes given in the start tag. + Attributes defaulted from the DTD follow the specified ones, + so attributes at lower indices in the list + passed to :attr:`StartElementHandler` were given in the start tag. + Each attribute takes two items in that list, + its name and its value. + Only meaningful inside a :attr:`StartElementHandler` call, + and only if :attr:`ordered_attributes` is true. + + .. versionadded:: next + + .. method:: xmlparser.GetInputContext() Returns the input data that generated the current event as a string. The data is diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index b017535b96979d..1d659812a8ebbf 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -613,6 +613,12 @@ xml instead of failing later, when encountering non-ASCII data. (Contributed by Serhiy Storchaka in :gh:`62259`.) +* Add :meth:`!GetSpecifiedAttributeCount` method + to the :mod:`XML parser ` objects. + It tells how many of the reported attributes were given in the start tag + rather than defaulted from the DTD. + (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) + zipfile ------- diff --git a/Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Rt7Nv2.rst b/Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Rt7Nv2.rst new file mode 100644 index 00000000000000..1da1d1a2fee6df --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Rt7Nv2.rst @@ -0,0 +1,4 @@ +:mod:`pyexpat` parser objects have a new method +:meth:`!GetSpecifiedAttributeCount`. +It tells how many of the attributes reported to :attr:`!StartElementHandler` +were given in the start tag rather than defaulted from the DTD. diff --git a/Modules/clinic/pyexpat.c.h b/Modules/clinic/pyexpat.c.h index 1a07726d303eca..9e32bb079c1ea6 100644 --- a/Modules/clinic/pyexpat.c.h +++ b/Modules/clinic/pyexpat.c.h @@ -212,6 +212,30 @@ pyexpat_xmlparser_GetBase(PyObject *self, PyObject *Py_UNUSED(ignored)) return pyexpat_xmlparser_GetBase_impl((xmlparseobject *)self); } +PyDoc_STRVAR(pyexpat_xmlparser_GetSpecifiedAttributeCount__doc__, +"GetSpecifiedAttributeCount($self, /)\n" +"--\n" +"\n" +"Return the index just past the attributes given in the start tag.\n" +"\n" +"Attributes defaulted from the DTD follow the specified ones, so\n" +"attributes at lower indices in the list passed to\n" +"StartElementHandler were given in the start tag. Each attribute\n" +"takes two items in that list, its name and its value. Only\n" +"meaningful inside a StartElementHandler call."); + +#define PYEXPAT_XMLPARSER_GETSPECIFIEDATTRIBUTECOUNT_METHODDEF \ + {"GetSpecifiedAttributeCount", (PyCFunction)pyexpat_xmlparser_GetSpecifiedAttributeCount, METH_NOARGS, pyexpat_xmlparser_GetSpecifiedAttributeCount__doc__}, + +static PyObject * +pyexpat_xmlparser_GetSpecifiedAttributeCount_impl(xmlparseobject *self); + +static PyObject * +pyexpat_xmlparser_GetSpecifiedAttributeCount(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return pyexpat_xmlparser_GetSpecifiedAttributeCount_impl((xmlparseobject *)self); +} + PyDoc_STRVAR(pyexpat_xmlparser_GetInputContext__doc__, "GetInputContext($self, /)\n" "--\n" @@ -839,4 +863,4 @@ pyexpat_ErrorString(PyObject *module, PyObject *arg) #ifndef PYEXPAT_XMLPARSER_SETALLOCTRACKERMAXIMUMAMPLIFICATION_METHODDEF #define PYEXPAT_XMLPARSER_SETALLOCTRACKERMAXIMUMAMPLIFICATION_METHODDEF #endif /* !defined(PYEXPAT_XMLPARSER_SETALLOCTRACKERMAXIMUMAMPLIFICATION_METHODDEF) */ -/*[clinic end generated code: output=270a0bfe3300e8a1 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d7e59d139fe45487 input=a9049054013a1b77]*/ diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 397a441f574fe4..fa8b0db6080623 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -1054,6 +1054,25 @@ pyexpat_xmlparser_GetBase_impl(xmlparseobject *self) return conv_string_to_unicode(XML_GetBase(self->itself)); } +/*[clinic input] +pyexpat.xmlparser.GetSpecifiedAttributeCount + +Return the index just past the attributes given in the start tag. + +Attributes defaulted from the DTD follow the specified ones, so +attributes at lower indices in the list passed to +StartElementHandler were given in the start tag. Each attribute +takes two items in that list, its name and its value. Only +meaningful inside a StartElementHandler call. +[clinic start generated code]*/ + +static PyObject * +pyexpat_xmlparser_GetSpecifiedAttributeCount_impl(xmlparseobject *self) +/*[clinic end generated code: output=f96b627de9393c0c input=4981c36cf99ebe9f]*/ +{ + return PyLong_FromLong(XML_GetSpecifiedAttributeCount(self->itself)); +} + /*[clinic input] @permit_long_summary pyexpat.xmlparser.GetInputContext @@ -1419,6 +1438,7 @@ static struct PyMethodDef xmlparse_methods[] = { PYEXPAT_XMLPARSER_PARSEFILE_METHODDEF PYEXPAT_XMLPARSER_SETBASE_METHODDEF PYEXPAT_XMLPARSER_GETBASE_METHODDEF + PYEXPAT_XMLPARSER_GETSPECIFIEDATTRIBUTECOUNT_METHODDEF PYEXPAT_XMLPARSER_GETINPUTCONTEXT_METHODDEF PYEXPAT_XMLPARSER_EXTERNALENTITYPARSERCREATE_METHODDEF PYEXPAT_XMLPARSER_SETPARAMENTITYPARSING_METHODDEF From f59bfabb47756a6e92dae852d298be40320dfa27 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 12 Aug 2026 21:56:59 +0300 Subject: [PATCH 2/2] gh-44871: Improve DOM Level 1 conformance of xml.dom.minidom * The factory methods now raise InvalidCharacterErr if the name is not a valid XML name. * appendChild(), insertBefore() and replaceChild() now raise WrongDocumentErr if the new child was created by other document, and HierarchyRequestErr if it is the node itself or its ancestor. * Attributes defaulted in the DTD are no longer omitted when parsing, and Attr.specified now reports whether the attribute was given in the start tag. * EntityReference nodes and Document.createEntityReference() are now implemented. --- Doc/library/xml.dom.minidom.rst | 9 +- Doc/library/xml.dom.rst | 27 +++++ Doc/whatsnew/3.16.rst | 21 ++++ Lib/test/test_minidom.py | 111 ++++++++++++++++++ Lib/xml/dom/expatbuilder.py | 5 +- Lib/xml/dom/minidom.py | 107 +++++++++++++++-- ...6-08-12-21-34-12.gh-issue-44871.Qw3Lm8.rst | 12 ++ 7 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Qw3Lm8.rst diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst index 1a5291d018ac70..fab22e9844ed5e 100644 --- a/Doc/library/xml.dom.minidom.rst +++ b/Doc/library/xml.dom.minidom.rst @@ -255,11 +255,14 @@ The following interfaces have no implementation in :mod:`!xml.dom.minidom`: * :class:`DOMTimeStamp` -* :class:`EntityReference` - -Most of these reflect information in the XML document that is not of general +This reflects information in the XML document that is not of general utility to most DOM users. +.. versionchanged:: next + :class:`EntityReference` is now implemented. + Note that the parser expands entity references, + so they only occur in a document if created explicitly. + .. rubric:: Footnotes .. [1] The encoding name included in the XML output should conform to diff --git a/Doc/library/xml.dom.rst b/Doc/library/xml.dom.rst index 34e58dcad93012..a79c38ee5ddff7 100644 --- a/Doc/library/xml.dom.rst +++ b/Doc/library/xml.dom.rst @@ -531,6 +531,14 @@ inherits properties from :class:`Node`. tree. +.. method:: Document.createEntityReference(name) + + Create and return a new entity reference node. + The node is not inserted into the document when it is created. + + .. versionadded:: next + + .. method:: Document.createComment(data) Create and return a comment node containing the data passed as a parameter. As @@ -800,6 +808,25 @@ Represents a processing instruction in the XML document; this inherits from the character. +.. _dom-entityreference-objects: + +EntityReference Objects +^^^^^^^^^^^^^^^^^^^^^^^ + +.. class:: EntityReference + + Represents an entity reference in the XML document. + It is a subclass of :class:`Node`. + The name of the referenced entity is its :attr:`nodeName`. + Its children are the replacement text of the entity, + and are read-only. + + Parsers may expand entity references, + so such a node only occurs in a document if it was created explicitly. + + .. versionadded:: next + + .. _dom-exceptions: Exceptions diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 1d659812a8ebbf..ae3eefeadaf09e 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -613,6 +613,16 @@ xml instead of failing later, when encountering non-ASCII data. (Contributed by Serhiy Storchaka in :gh:`62259`.) +* :mod:`xml.dom.minidom` now conforms closer to the DOM Level 1 specification. + It checks names passed to the factory methods, + rejects inserting a node created by other document + or making a node a descendant of itself, + reports attributes defaulted in the DTD + and whether an attribute was given in the start tag, + and implements :class:`!EntityReference` nodes + and :meth:`!Document.createEntityReference`. + (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) + * Add :meth:`!GetSpecifiedAttributeCount` method to the :mod:`XML parser ` objects. It tells how many of the reported attributes were given in the start tag @@ -834,6 +844,17 @@ that may require changes to your code. :exc:`TypeError`. (Contributed by Serhiy Storchaka in :gh:`152587`.) +* :mod:`xml.dom.minidom` now raises :exc:`~xml.dom.InvalidCharacterErr` + for a name which is not a valid XML name, + :exc:`~xml.dom.WrongDocumentErr` + for inserting a node created by other document, + and :exc:`~xml.dom.HierarchyRequestErr` for inserting a node into itself + or its descendant. + Such operations formerly succeeded + and produced an invalid document or an endless loop. + Attributes defaulted in the DTD are no longer omitted when parsing. + (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) + * On Windows, seeking a pipe now fails instead of silently appearing to succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`, and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence, diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 46249e5138aed5..52f9adec90e02c 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -1784,5 +1784,116 @@ def test_cdata_parsing(self): dom2 = parseString(dom1.toprettyxml()) self.checkWholeText(dom2.getElementsByTagName('node')[0].firstChild, '') + def testInvalidCharacterErr(self): + doc = parseString("") + impl = getDOMImplementation() + for name in ("", "bad name", "1st", "-x", ".x", "a") + other = parseString("") + elem = doc.documentElement + alien = other.createElement("alien") + self.assertRaises(xml.dom.WrongDocumentErr, elem.appendChild, alien) + self.assertRaises(xml.dom.WrongDocumentErr, elem.insertBefore, + alien, elem.firstChild) + self.assertRaises(xml.dom.WrongDocumentErr, elem.replaceChild, + alien, elem.firstChild) + self.assertRaises(xml.dom.WrongDocumentErr, doc.appendChild, alien) + # the rejected node is left alone + self.assertIs(alien.ownerDocument, other) + self.assertIsNone(alien.parentNode) + # importNode() is the supported way to do this + elem.appendChild(doc.importNode(alien, True)) + self.assertEqual(elem.lastChild.tagName, "alien") + doc.unlink() + other.unlink() + + def testAncestorLoops(self): + doc = parseString("") + elem = doc.documentElement + child = elem.firstChild + grandchild = child.firstChild + for node in elem, child, grandchild: + self.assertRaises(xml.dom.HierarchyRequestErr, + node.appendChild, node) + self.assertRaises(xml.dom.HierarchyRequestErr, child.appendChild, elem) + self.assertRaises(xml.dom.HierarchyRequestErr, + grandchild.appendChild, elem) + self.assertRaises(xml.dom.HierarchyRequestErr, + grandchild.insertBefore, child, None) + self.assertRaises(xml.dom.HierarchyRequestErr, + grandchild.replaceChild, elem, None) + # the tree is unchanged + self.assertIs(child.parentNode, elem) + self.assertIs(grandchild.parentNode, child) + doc.unlink() + + def testAttrSpecified(self): + doc = parseString("" + " " + "]>") + elem = doc.documentElement + # attributes defaulted from the DTD are reported too + self.assertEqual(sorted(elem.attributes.keys()), ["a", "b"]) + self.assertEqual(elem.getAttribute("a"), "default") + self.assertFalse(elem.getAttributeNode("a").specified) + self.assertEqual(elem.getAttribute("b"), "given") + self.assertTrue(elem.getAttributeNode("b").specified) + doc.unlink() + + def testEntityReference(self): + doc = parseString("") + ref = doc.createEntityReference("ent") + self.assertEqual(ref.nodeType, Node.ENTITY_REFERENCE_NODE) + self.assertEqual(ref.nodeName, "ent") + self.assertIsNone(ref.nodeValue) + self.assertIs(ref.ownerDocument, doc) + doc.documentElement.appendChild(ref) + self.assertEqual(doc.documentElement.toxml(), "&ent;") + # entity reference nodes are read-only + text = doc.createTextNode("x") + self.assertRaises(xml.dom.NoModificationAllowedErr, + ref.appendChild, text) + self.assertRaises(xml.dom.NoModificationAllowedErr, + ref.insertBefore, text, None) + self.assertRaises(xml.dom.NoModificationAllowedErr, + ref.removeChild, text) + self.assertRaises(xml.dom.NoModificationAllowedErr, + ref.replaceChild, text, None) + self.assertEqual(ref.cloneNode(True).nodeName, "ent") + other = parseString("") + self.assertEqual(other.importNode(ref, True).nodeName, "ent") + doc.unlink() + other.unlink() + + if __name__ == "__main__": unittest.main() diff --git a/Lib/xml/dom/expatbuilder.py b/Lib/xml/dom/expatbuilder.py index 7dd667bf3fbe04..d56b2ddfdb2569 100644 --- a/Lib/xml/dom/expatbuilder.py +++ b/Lib/xml/dom/expatbuilder.py @@ -159,7 +159,6 @@ def getParser(self): self._intern_setdefault = self._parser.intern.setdefault self._parser.buffer_text = True self._parser.ordered_attributes = True - self._parser.specified_attributes = True self.install(self._parser) return self._parser @@ -352,11 +351,13 @@ def start_element_handler(self, name, attributes): self.curNode = node if attributes: + specified = self.getParser().GetSpecifiedAttributeCount() for i in range(0, len(attributes), 2): a = minidom.Attr(attributes[i], EMPTY_NAMESPACE, None, EMPTY_PREFIX) value = attributes[i+1] a.value = value + a.specified = i < specified a.ownerDocument = self.document _set_attribute_node(node, a) @@ -760,6 +761,7 @@ def start_element_handler(self, name, attributes): node._ensure_attributes() _attrs = node._attrs _attrsNS = node._attrsNS + specified = self.getParser().GetSpecifiedAttributeCount() for i in range(0, len(attributes), 2): aname = attributes[i] value = attributes[i+1] @@ -775,6 +777,7 @@ def start_element_handler(self, name, attributes): _attrsNS[(EMPTY_NAMESPACE, aname)] = a a.ownerDocument = self.document a.value = value + a.specified = i < specified a.ownerElement = node if __debug__: diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 16b33b90184dc5..a09e1afb343d57 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -16,6 +16,7 @@ """ import io +import xml import xml.dom from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg @@ -79,15 +80,30 @@ def _get_lastChild(self): if self.childNodes: return self.childNodes[-1] + def _check_new_child(self, newChild): + # Common checks for insertBefore(), appendChild() and replaceChild(). + doc = self.ownerDocument or self + newChildDoc = newChild.ownerDocument + if newChildDoc is not doc and newChildDoc is not None: + raise xml.dom.WrongDocumentErr( + "%s was created by a different document" % repr(newChild)) + if newChild.nodeType not in self._child_node_types: + raise xml.dom.HierarchyRequestErr( + "%s cannot be child of %s" % (repr(newChild), repr(self))) + if newChild is self: + raise xml.dom.HierarchyRequestErr( + "%s cannot be child of itself" % repr(self)) + if _is_ancestor(newChild, self): + raise xml.dom.HierarchyRequestErr( + "%s is an ancestor of %s" % (repr(newChild), repr(self))) + def insertBefore(self, newChild, refChild): if newChild.nodeType == self.DOCUMENT_FRAGMENT_NODE: for c in tuple(newChild.childNodes): self.insertBefore(c, refChild) ### The DOM does not clearly specify what to return in this case return newChild - if newChild.nodeType not in self._child_node_types: - raise xml.dom.HierarchyRequestErr( - "%s cannot be child of %s" % (repr(newChild), repr(self))) + self._check_new_child(newChild) if newChild.parentNode is not None: newChild.parentNode.removeChild(newChild) if refChild is None: @@ -117,10 +133,8 @@ def appendChild(self, node): self.appendChild(c) ### The DOM does not clearly specify what to return in this case return node - if node.nodeType not in self._child_node_types: - raise xml.dom.HierarchyRequestErr( - "%s cannot be child of %s" % (repr(node), repr(self))) - elif node.nodeType in _nodeTypes_with_children: + self._check_new_child(node) + if node.nodeType in _nodeTypes_with_children: _clear_id_cache(self) if node.parentNode is not None: node.parentNode.removeChild(node) @@ -133,11 +147,9 @@ def replaceChild(self, newChild, oldChild): refChild = oldChild.nextSibling self.removeChild(oldChild) return self.insertBefore(newChild, refChild) - if newChild.nodeType not in self._child_node_types: - raise xml.dom.HierarchyRequestErr( - "%s cannot be child of %s" % (repr(newChild), repr(self))) if newChild is oldChild: return + self._check_new_child(newChild) if newChild.parentNode is not None: newChild.parentNode.removeChild(newChild) try: @@ -282,6 +294,22 @@ def __exit__(self, et, ev, tb): defproperty(Node, "localName", doc="Namespace-local name of this node.") +def _check_name(name): + if not xml.is_valid_name(name): + raise xml.dom.InvalidCharacterErr( + "%r is not a valid XML name" % (name,)) + + +def _is_ancestor(node, other): + "Returns true iff node is an ancestor of other." + other = other.parentNode + while other is not None: + if other is node: + return True + other = other.parentNode + return False + + def _append_child(self, node): # fast path with less checks; usable by DOM builders if careful childNodes = self.childNodes @@ -758,6 +786,7 @@ def getAttributeNS(self, namespaceURI, localName): def setAttribute(self, attname, value): attr = self.getAttributeNode(attname) if attr is None: + _check_name(attname) attr = Attr(attname) attr.value = value # also sets nodeValue attr.ownerDocument = self.ownerDocument @@ -771,6 +800,7 @@ def setAttributeNS(self, namespaceURI, qualifiedName, value): prefix, localname = _nssplit(qualifiedName) attr = self.getAttributeNodeNS(namespaceURI, localname) if attr is None: + _check_name(qualifiedName) attr = Attr(qualifiedName, namespaceURI, localname, prefix) attr.value = value attr.ownerDocument = self.ownerDocument @@ -1416,6 +1446,45 @@ def replaceChild(self, newChild, oldChild): raise xml.dom.HierarchyRequestErr( "cannot replace children of an entity node") +class EntityReference(Node): + nodeType = Node.ENTITY_REFERENCE_NODE + nodeValue = None + attributes = None + + _child_node_types = (Node.ELEMENT_NODE, + Node.PROCESSING_INSTRUCTION_NODE, + Node.COMMENT_NODE, + Node.TEXT_NODE, + Node.CDATA_SECTION_NODE, + Node.ENTITY_REFERENCE_NODE) + + def __init__(self, name): + self.nodeName = name + self.childNodes = NodeList() + + def _get_nodeName(self): + return self.nodeName + + def appendChild(self, newChild): + raise xml.dom.NoModificationAllowedErr( + "entity reference nodes are read-only") + + def insertBefore(self, newChild, refChild): + raise xml.dom.NoModificationAllowedErr( + "entity reference nodes are read-only") + + def removeChild(self, oldChild): + raise xml.dom.NoModificationAllowedErr( + "entity reference nodes are read-only") + + def replaceChild(self, newChild, oldChild): + raise xml.dom.NoModificationAllowedErr( + "entity reference nodes are read-only") + + def writexml(self, writer, indent="", addindent="", newl=""): + writer.write("&%s;" % self.nodeName) + + class Notation(Identified, Childless, Node): nodeType = Node.NOTATION_NODE nodeValue = None @@ -1487,6 +1556,7 @@ def createDocument(self, namespaceURI, qualifiedName, doctype): return doc def createDocumentType(self, qualifiedName, publicId, systemId): + _check_name(qualifiedName) doctype = DocumentType(qualifiedName) doctype.publicId = publicId doctype.systemId = systemId @@ -1622,9 +1692,7 @@ def _get_version(self): return self.version def appendChild(self, node): - if node.nodeType not in self._child_node_types: - raise xml.dom.HierarchyRequestErr( - "%s cannot be child of %s" % (repr(node), repr(self))) + self._check_new_child(node) if node.parentNode is not None: # This needs to be done before the next test since this # may *be* the document element, in which case it should @@ -1687,6 +1755,7 @@ def createDocumentFragment(self): return d def createElement(self, tagName): + _check_name(tagName) e = Element(tagName) e.ownerDocument = self return e @@ -1707,29 +1776,39 @@ def createCDATASection(self, data): c.ownerDocument = self return c + def createEntityReference(self, name): + _check_name(name) + e = EntityReference(name) + e.ownerDocument = self + return e + def createComment(self, data): c = Comment(data) c.ownerDocument = self return c def createProcessingInstruction(self, target, data): + _check_name(target) p = ProcessingInstruction(target, data) p.ownerDocument = self return p def createAttribute(self, qName): + _check_name(qName) a = Attr(qName) a.ownerDocument = self a.value = "" return a def createElementNS(self, namespaceURI, qualifiedName): + _check_name(qualifiedName) prefix, localName = _nssplit(qualifiedName) e = Element(qualifiedName, namespaceURI, prefix) e.ownerDocument = self return e def createAttributeNS(self, namespaceURI, qualifiedName): + _check_name(qualifiedName) prefix, localName = _nssplit(qualifiedName) a = Attr(qualifiedName, namespaceURI, localName, prefix) a.ownerDocument = self @@ -1935,6 +2014,8 @@ def _clone_node(node, deep, newOwnerDocument): node.data) elif node.nodeType == Node.COMMENT_NODE: clone = newOwnerDocument.createComment(node.data) + 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) diff --git a/Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Qw3Lm8.rst b/Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Qw3Lm8.rst new file mode 100644 index 00000000000000..2fe6bc5bada3b9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-12-21-34-12.gh-issue-44871.Qw3Lm8.rst @@ -0,0 +1,12 @@ +:mod:`xml.dom.minidom` now conforms closer to the DOM Level 1 specification. +The factory methods now raise :exc:`~xml.dom.InvalidCharacterErr` +if the name is not a valid XML name. +Inserting a node created by other document +now raises :exc:`~xml.dom.WrongDocumentErr`, +and inserting a node into itself or its descendant +now raises :exc:`~xml.dom.HierarchyRequestErr`. +Attributes defaulted in the DTD are no longer omitted when parsing, +and :attr:`!Attr.specified` now reports +whether the attribute was given in the start tag. +:class:`!EntityReference` nodes +and :meth:`!Document.createEntityReference` are now implemented.