Skip to content

Commit f59bfab

Browse files
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.
1 parent feabab3 commit f59bfab

7 files changed

Lines changed: 275 additions & 17 deletions

File tree

Doc/library/xml.dom.minidom.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,14 @@ The following interfaces have no implementation in :mod:`!xml.dom.minidom`:
255255

256256
* :class:`DOMTimeStamp`
257257

258-
* :class:`EntityReference`
259-
260-
Most of these reflect information in the XML document that is not of general
258+
This reflects information in the XML document that is not of general
261259
utility to most DOM users.
262260

261+
.. versionchanged:: next
262+
:class:`EntityReference` is now implemented.
263+
Note that the parser expands entity references,
264+
so they only occur in a document if created explicitly.
265+
263266
.. rubric:: Footnotes
264267

265268
.. [1] The encoding name included in the XML output should conform to

Doc/library/xml.dom.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,14 @@ inherits properties from :class:`Node`.
531531
tree.
532532

533533

534+
.. method:: Document.createEntityReference(name)
535+
536+
Create and return a new entity reference node.
537+
The node is not inserted into the document when it is created.
538+
539+
.. versionadded:: next
540+
541+
534542
.. method:: Document.createComment(data)
535543

536544
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
800808
character.
801809

802810

811+
.. _dom-entityreference-objects:
812+
813+
EntityReference Objects
814+
^^^^^^^^^^^^^^^^^^^^^^^
815+
816+
.. class:: EntityReference
817+
818+
Represents an entity reference in the XML document.
819+
It is a subclass of :class:`Node`.
820+
The name of the referenced entity is its :attr:`nodeName`.
821+
Its children are the replacement text of the entity,
822+
and are read-only.
823+
824+
Parsers may expand entity references,
825+
so such a node only occurs in a document if it was created explicitly.
826+
827+
.. versionadded:: next
828+
829+
803830
.. _dom-exceptions:
804831

805832
Exceptions

Doc/whatsnew/3.16.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,16 @@ xml
613613
instead of failing later, when encountering non-ASCII data.
614614
(Contributed by Serhiy Storchaka in :gh:`62259`.)
615615

616+
* :mod:`xml.dom.minidom` now conforms closer to the DOM Level 1 specification.
617+
It checks names passed to the factory methods,
618+
rejects inserting a node created by other document
619+
or making a node a descendant of itself,
620+
reports attributes defaulted in the DTD
621+
and whether an attribute was given in the start tag,
622+
and implements :class:`!EntityReference` nodes
623+
and :meth:`!Document.createEntityReference`.
624+
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
625+
616626
* Add :meth:`!GetSpecifiedAttributeCount` method
617627
to the :mod:`XML parser <xml.parsers.expat>` objects.
618628
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.
834844
:exc:`TypeError`.
835845
(Contributed by Serhiy Storchaka in :gh:`152587`.)
836846

847+
* :mod:`xml.dom.minidom` now raises :exc:`~xml.dom.InvalidCharacterErr`
848+
for a name which is not a valid XML name,
849+
:exc:`~xml.dom.WrongDocumentErr`
850+
for inserting a node created by other document,
851+
and :exc:`~xml.dom.HierarchyRequestErr` for inserting a node into itself
852+
or its descendant.
853+
Such operations formerly succeeded
854+
and produced an invalid document or an endless loop.
855+
Attributes defaulted in the DTD are no longer omitted when parsing.
856+
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
857+
837858
* On Windows, seeking a pipe now fails instead of silently appearing to
838859
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
839860
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,

Lib/test/test_minidom.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,5 +1784,116 @@ def test_cdata_parsing(self):
17841784
dom2 = parseString(dom1.toprettyxml())
17851785
self.checkWholeText(dom2.getElementsByTagName('node')[0].firstChild, '</data>')
17861786

1787+
def testInvalidCharacterErr(self):
1788+
doc = parseString("<doc/>")
1789+
impl = getDOMImplementation()
1790+
for name in ("", "bad name", "1st", "-x", ".x", "a<b", "a&b", "a\tb"):
1791+
with self.subTest(name=name):
1792+
self.assertRaises(xml.dom.InvalidCharacterErr,
1793+
doc.createElement, name)
1794+
self.assertRaises(xml.dom.InvalidCharacterErr,
1795+
doc.createElementNS, None, name)
1796+
self.assertRaises(xml.dom.InvalidCharacterErr,
1797+
doc.createAttribute, name)
1798+
self.assertRaises(xml.dom.InvalidCharacterErr,
1799+
doc.createAttributeNS, None, name)
1800+
self.assertRaises(xml.dom.InvalidCharacterErr,
1801+
doc.createProcessingInstruction, name, "")
1802+
self.assertRaises(xml.dom.InvalidCharacterErr,
1803+
doc.createEntityReference, name)
1804+
self.assertRaises(xml.dom.InvalidCharacterErr,
1805+
impl.createDocumentType, name, None, None)
1806+
self.assertRaises(xml.dom.InvalidCharacterErr,
1807+
doc.documentElement.setAttribute, name, "v")
1808+
self.assertRaises(xml.dom.InvalidCharacterErr,
1809+
doc.documentElement.setAttributeNS,
1810+
None, name, "v")
1811+
for name in ("a", "_x", ":x", "a.b-c", "ns:tag", "a1",
1812+
"\N{GREEK CAPITAL LETTER OMEGA}", "\N{LINEAR B SYLLABLE B008 A}x"):
1813+
with self.subTest(name=name):
1814+
self.assertEqual(doc.createElement(name).tagName, name)
1815+
self.assertEqual(doc.createAttribute(name).name, name)
1816+
doc.unlink()
1817+
1818+
def testWrongDocumentErr(self):
1819+
doc = parseString("<doc><child/></doc>")
1820+
other = parseString("<other/>")
1821+
elem = doc.documentElement
1822+
alien = other.createElement("alien")
1823+
self.assertRaises(xml.dom.WrongDocumentErr, elem.appendChild, alien)
1824+
self.assertRaises(xml.dom.WrongDocumentErr, elem.insertBefore,
1825+
alien, elem.firstChild)
1826+
self.assertRaises(xml.dom.WrongDocumentErr, elem.replaceChild,
1827+
alien, elem.firstChild)
1828+
self.assertRaises(xml.dom.WrongDocumentErr, doc.appendChild, alien)
1829+
# the rejected node is left alone
1830+
self.assertIs(alien.ownerDocument, other)
1831+
self.assertIsNone(alien.parentNode)
1832+
# importNode() is the supported way to do this
1833+
elem.appendChild(doc.importNode(alien, True))
1834+
self.assertEqual(elem.lastChild.tagName, "alien")
1835+
doc.unlink()
1836+
other.unlink()
1837+
1838+
def testAncestorLoops(self):
1839+
doc = parseString("<doc><child><grandchild/></child></doc>")
1840+
elem = doc.documentElement
1841+
child = elem.firstChild
1842+
grandchild = child.firstChild
1843+
for node in elem, child, grandchild:
1844+
self.assertRaises(xml.dom.HierarchyRequestErr,
1845+
node.appendChild, node)
1846+
self.assertRaises(xml.dom.HierarchyRequestErr, child.appendChild, elem)
1847+
self.assertRaises(xml.dom.HierarchyRequestErr,
1848+
grandchild.appendChild, elem)
1849+
self.assertRaises(xml.dom.HierarchyRequestErr,
1850+
grandchild.insertBefore, child, None)
1851+
self.assertRaises(xml.dom.HierarchyRequestErr,
1852+
grandchild.replaceChild, elem, None)
1853+
# the tree is unchanged
1854+
self.assertIs(child.parentNode, elem)
1855+
self.assertIs(grandchild.parentNode, child)
1856+
doc.unlink()
1857+
1858+
def testAttrSpecified(self):
1859+
doc = parseString("<!DOCTYPE doc ["
1860+
" <!ELEMENT doc EMPTY>"
1861+
" <!ATTLIST doc a CDATA 'default' b CDATA #IMPLIED>"
1862+
"]><doc b='given'/>")
1863+
elem = doc.documentElement
1864+
# attributes defaulted from the DTD are reported too
1865+
self.assertEqual(sorted(elem.attributes.keys()), ["a", "b"])
1866+
self.assertEqual(elem.getAttribute("a"), "default")
1867+
self.assertFalse(elem.getAttributeNode("a").specified)
1868+
self.assertEqual(elem.getAttribute("b"), "given")
1869+
self.assertTrue(elem.getAttributeNode("b").specified)
1870+
doc.unlink()
1871+
1872+
def testEntityReference(self):
1873+
doc = parseString("<doc/>")
1874+
ref = doc.createEntityReference("ent")
1875+
self.assertEqual(ref.nodeType, Node.ENTITY_REFERENCE_NODE)
1876+
self.assertEqual(ref.nodeName, "ent")
1877+
self.assertIsNone(ref.nodeValue)
1878+
self.assertIs(ref.ownerDocument, doc)
1879+
doc.documentElement.appendChild(ref)
1880+
self.assertEqual(doc.documentElement.toxml(), "<doc>&ent;</doc>")
1881+
# entity reference nodes are read-only
1882+
text = doc.createTextNode("x")
1883+
self.assertRaises(xml.dom.NoModificationAllowedErr,
1884+
ref.appendChild, text)
1885+
self.assertRaises(xml.dom.NoModificationAllowedErr,
1886+
ref.insertBefore, text, None)
1887+
self.assertRaises(xml.dom.NoModificationAllowedErr,
1888+
ref.removeChild, text)
1889+
self.assertRaises(xml.dom.NoModificationAllowedErr,
1890+
ref.replaceChild, text, None)
1891+
self.assertEqual(ref.cloneNode(True).nodeName, "ent")
1892+
other = parseString("<other/>")
1893+
self.assertEqual(other.importNode(ref, True).nodeName, "ent")
1894+
doc.unlink()
1895+
other.unlink()
1896+
1897+
17871898
if __name__ == "__main__":
17881899
unittest.main()

Lib/xml/dom/expatbuilder.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ def getParser(self):
159159
self._intern_setdefault = self._parser.intern.setdefault
160160
self._parser.buffer_text = True
161161
self._parser.ordered_attributes = True
162-
self._parser.specified_attributes = True
163162
self.install(self._parser)
164163
return self._parser
165164

@@ -352,11 +351,13 @@ def start_element_handler(self, name, attributes):
352351
self.curNode = node
353352

354353
if attributes:
354+
specified = self.getParser().GetSpecifiedAttributeCount()
355355
for i in range(0, len(attributes), 2):
356356
a = minidom.Attr(attributes[i], EMPTY_NAMESPACE,
357357
None, EMPTY_PREFIX)
358358
value = attributes[i+1]
359359
a.value = value
360+
a.specified = i < specified
360361
a.ownerDocument = self.document
361362
_set_attribute_node(node, a)
362363

@@ -760,6 +761,7 @@ def start_element_handler(self, name, attributes):
760761
node._ensure_attributes()
761762
_attrs = node._attrs
762763
_attrsNS = node._attrsNS
764+
specified = self.getParser().GetSpecifiedAttributeCount()
763765
for i in range(0, len(attributes), 2):
764766
aname = attributes[i]
765767
value = attributes[i+1]
@@ -775,6 +777,7 @@ def start_element_handler(self, name, attributes):
775777
_attrsNS[(EMPTY_NAMESPACE, aname)] = a
776778
a.ownerDocument = self.document
777779
a.value = value
780+
a.specified = i < specified
778781
a.ownerElement = node
779782

780783
if __debug__:

0 commit comments

Comments
 (0)