Skip to content

Commit cd1c027

Browse files
gh-57336: Deprecate setting read-only attributes in xml.dom.minidom
In the DOM model most node attributes are read-only, but in minidom only localName and length raised NoModificationAllowedErr. Setting the other ones silently left the node inconsistent with the document: for example, setting Attr.name renamed the attribute node, but the owner element still kept it under the old name. Setting them now emits a DeprecationWarning. They are also documented as read-only.
1 parent b11e749 commit cd1c027

6 files changed

Lines changed: 225 additions & 57 deletions

File tree

Doc/library/xml.dom.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ Node Objects
235235

236236
All of the components of an XML document are subclasses of :class:`Node`.
237237

238+
.. versionchanged:: next
239+
Setting a read-only attribute is deprecated.
240+
It now emits a :exc:`DeprecationWarning`
241+
and will raise :exc:`NoModificationAllowedErr` in a future version of Python.
242+
238243

239244
.. attribute:: Node.nodeType
240245

@@ -306,6 +311,7 @@ All of the components of an XML document are subclasses of :class:`Node`.
306311

307312
The part of the :attr:`tagName` preceding the colon if there is one, else the
308313
empty string. The value is a string, or ``None``.
314+
This is a read-only attribute.
309315

310316

311317
.. attribute:: Node.namespaceURI
@@ -455,12 +461,14 @@ following attributes:
455461

456462
The public identifier for the external subset of the document type definition.
457463
This will be a string or ``None``.
464+
This is a read-only attribute.
458465

459466

460467
.. attribute:: DocumentType.systemId
461468

462469
The system identifier for the external subset of the document type definition.
463470
This will be a URI as a string, or ``None``.
471+
This is a read-only attribute.
464472

465473

466474
.. attribute:: DocumentType.internalSubset
@@ -474,6 +482,7 @@ following attributes:
474482

475483
The name of the root element as given in the ``DOCTYPE`` declaration, if
476484
present.
485+
This is a read-only attribute.
477486

478487

479488
.. attribute:: DocumentType.entities
@@ -587,6 +596,7 @@ of that class.
587596

588597
The element type name. In a namespace-using document it may have colons in it.
589598
The value is a string.
599+
This is a read-only attribute.
590600

591601

592602
.. method:: Element.getElementsByTagName(tagName)
@@ -690,6 +700,7 @@ Attr Objects
690700

691701
The attribute name.
692702
In a namespace-using document it may include a colon.
703+
This is a read-only attribute.
693704

694705

695706
.. attribute:: Attr.localName
@@ -703,6 +714,7 @@ Attr Objects
703714

704715
The part of the name preceding the colon if there is one, else the
705716
empty string.
717+
This is a read-only attribute.
706718

707719

708720
.. attribute:: Attr.value

Doc/whatsnew/3.16.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,18 @@ New deprecations
799799
open them one by one instead.
800800
(Contributed by Serhiy Storchaka in :gh:`152638`.)
801801

802+
* :mod:`xml.dom.minidom`:
803+
804+
* Setting a read-only attribute of a node is deprecated.
805+
It now emits a :exc:`DeprecationWarning`
806+
and will raise :exc:`xml.dom.NoModificationAllowedErr` in the future.
807+
This affects the :attr:`!nodeType`, :attr:`!nodeName`, :attr:`!name`,
808+
:attr:`!tagName`, :attr:`!target`, :attr:`!prefix`, :attr:`!namespaceURI`,
809+
:attr:`!publicId` and :attr:`!systemId` attributes.
810+
Use :meth:`~xml.dom.Document.renameNode` to rename an element
811+
or an attribute.
812+
(Contributed by Serhiy Storchaka in :gh:`57336`.)
813+
802814
.. Add deprecations above alphabetically, not here at the end.
803815
804816
.. include:: ../deprecations/pending-removal-in-3.17.rst

Lib/test/test_minidom.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pickle
55
import io
66
from test import support
7+
from test.support import warnings_helper
78
import unittest
89

910
import xml.dom.minidom
@@ -777,7 +778,9 @@ def _setupCloneElement(self, deep):
777778
self._testCloneElementCopiesAttributes(
778779
root, clone, "testCloneElement" + (deep and "Deep" or "Shallow"))
779780
# mutilate the original so shared data is detected
780-
root.tagName = root.nodeName = "MODIFIED"
781+
with warnings_helper.check_warnings(
782+
('', DeprecationWarning), quiet=True):
783+
root.tagName = root.nodeName = "MODIFIED"
781784
root.setAttribute("attr", "NEW VALUE")
782785
root.setAttribute("added", "VALUE")
783786
return dom, clone
@@ -1325,6 +1328,38 @@ def checkRenameNodeSharedConstraints(self, doc, node):
13251328
self.assertRaises(xml.dom.WrongDocumentErr, doc2.renameNode, node,
13261329
xml.dom.EMPTY_NAMESPACE, "foo")
13271330

1331+
def test_readonly_attributes(self):
1332+
# These attributes are read-only in the DOM, and setting them
1333+
# is deprecated (gh-57336).
1334+
doc = parseString('<!DOCTYPE doc PUBLIC "p" "s">'
1335+
'<doc a="v">text<!--c--><?pi d?><![CDATA[x]]></doc>')
1336+
elem = doc.documentElement
1337+
attr = elem.attributes["a"]
1338+
text, comment, pi, cdata = elem.childNodes
1339+
for node, name in [
1340+
(doc, "nodeType"), (doc, "nodeName"),
1341+
(doc.doctype, "name"), (doc.doctype, "nodeName"),
1342+
(doc.doctype, "publicId"), (doc.doctype, "systemId"),
1343+
(elem, "nodeType"),
1344+
(elem, "tagName"), (elem, "nodeName"),
1345+
(elem, "prefix"), (elem, "namespaceURI"),
1346+
(attr, "name"), (attr, "nodeName"),
1347+
(attr, "prefix"), (attr, "namespaceURI"),
1348+
(text, "nodeName"), (comment, "nodeName"), (cdata, "nodeName"),
1349+
(pi, "nodeName"), (pi, "target"),
1350+
]:
1351+
with self.subTest(node=type(node).__name__, name=name):
1352+
value = getattr(node, name)
1353+
with self.assertWarns(DeprecationWarning):
1354+
setattr(node, name, value)
1355+
self.assertEqual(getattr(node, name), value)
1356+
1357+
# These are writable.
1358+
attr.value = "other"
1359+
text.data = "other"
1360+
self.assertEqual(attr.value, "other")
1361+
self.assertEqual(text.data, "other")
1362+
13281363
def testRenameAttribute(self):
13291364
doc = parseString("<doc a='v'/>")
13301365
elem = doc.documentElement

Lib/xml/dom/minicompat.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
# defproperty() should be used for each version of
4141
# the relevant _get_<property>() function.
4242

43-
__all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty"]
43+
__all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty",
44+
"defdeprecatedproperty"]
4445

4546
import xml.dom
4647

@@ -107,3 +108,22 @@ def set(self, value, name=name):
107108
"expected not to find _set_" + name
108109
prop = property(get, set, doc=doc)
109110
setattr(klass, name, prop)
111+
112+
113+
def defdeprecatedproperty(klass, name, doc, private=None):
114+
"""Define a read-only attribute whose setter is deprecated.
115+
116+
The value is stored in the *private* attribute ("_" + name by default).
117+
Setting the attribute still works, but emits a DeprecationWarning.
118+
"""
119+
if private is None:
120+
private = "_" + name
121+
def get(self, private=private):
122+
return getattr(self, private)
123+
def set(self, value, name=name, private=private):
124+
import warnings
125+
warnings.warn(f"attempt to modify read-only attribute {name!r} "
126+
f"is deprecated", DeprecationWarning, stacklevel=2)
127+
setattr(self, private, value)
128+
prop = property(get, set, doc=doc)
129+
setattr(klass, name, prop)

0 commit comments

Comments
 (0)