]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145056: Accept frozendict in xml.etree (#145508)
authorVictor Stinner <vstinner@python.org>
Thu, 5 Mar 2026 11:55:28 +0000 (12:55 +0100)
committerGitHub <noreply@github.com>
Thu, 5 Mar 2026 11:55:28 +0000 (12:55 +0100)
Element and SubElement of xml.etree.ElementTree now also accept
frozendict for attrib.

Export _PyDict_CopyAsDict() function.

Doc/library/xml.etree.elementtree.rst
Include/internal/pycore_dict.h
Lib/test/test_xml_etree.py
Lib/xml/etree/ElementTree.py
Modules/_elementtree.c

index e021a81d2a2b87b235961002070ed018d7a7cd7c..45abf5b1e736b3d9e1ea598962700b80a938d14c 100644 (file)
@@ -702,6 +702,9 @@ Functions
    attributes.  *extra* contains additional attributes, given as keyword
    arguments.  Returns an element instance.
 
+   .. versionchanged:: next
+      *attrib* can now be a :class:`frozendict`.
+
 
 .. function:: tostring(element, encoding="us-ascii", method="xml", *, \
                        xml_declaration=None, default_namespace=None, \
@@ -887,6 +890,9 @@ Element Objects
    an optional dictionary, containing element attributes.  *extra* contains
    additional attributes, given as keyword arguments.
 
+   .. versionchanged:: next
+      *attrib* can now be a :class:`frozendict`.
+
 
    .. attribute:: tag
 
index a2c5ee41c37784bff05a6294ad83b1fbb03a1e25..6d7d68eda84c5afdc64dd21d2e3bd5e9530f9bee 100644 (file)
@@ -160,7 +160,8 @@ extern void _PyDict_Clear_LockHeld(PyObject *op);
 PyAPI_FUNC(void) _PyDict_EnsureSharedOnRead(PyDictObject *mp);
 #endif
 
-extern PyObject* _PyDict_CopyAsDict(PyObject *op);
+// Export for '_elementtree' shared extension
+PyAPI_FUNC(PyObject*) _PyDict_CopyAsDict(PyObject *op);
 
 #define DKIX_EMPTY (-1)
 #define DKIX_DUMMY (-2)  /* Used internally */
index 93162f52ba03440d521e272a4b639b8883868ba8..5b06e422672b1dde3891db6fec465fcd00e8d9a9 100644 (file)
@@ -4472,6 +4472,9 @@ class KeywordArgsTest(unittest.TestCase):
             ET.Element('a', dict(href="#"), id="foo"),
             ET.Element('a', href="#", id="foo"),
             ET.Element('a', dict(href="#", id="foo"), href="#", id="foo"),
+            ET.Element('a', frozendict(href="#", id="foo")),
+            ET.Element('a', frozendict(href="#"), id="foo"),
+            ET.Element('a', attrib=frozendict(href="#", id="foo")),
         ]
         for e in elements:
             self.assertEqual(e.tag, 'a')
@@ -4479,10 +4482,14 @@ class KeywordArgsTest(unittest.TestCase):
 
         e2 = ET.SubElement(elements[0], 'foobar', attrib={'key1': 'value1'})
         self.assertEqual(e2.attrib['key1'], 'value1')
+        e3 = ET.SubElement(elements[0], 'foobar',
+                           attrib=frozendict({'key1': 'value1'}))
+        self.assertEqual(e3.attrib['key1'], 'value1')
 
-        with self.assertRaisesRegex(TypeError, 'must be dict, not str'):
+        errmsg = 'must be dict or frozendict, not str'
+        with self.assertRaisesRegex(TypeError, errmsg):
             ET.Element('a', "I'm not a dict")
-        with self.assertRaisesRegex(TypeError, 'must be dict, not str'):
+        with self.assertRaisesRegex(TypeError, errmsg):
             ET.Element('a', attrib="I'm not a dict")
 
 # --------------------------------------------------------------------
index e3d81a2c4560d97e6de6712533f97fe1a0774a9a..57c5b64ea3ba70a99009b194275bd94f1fe96455 100644 (file)
@@ -165,8 +165,8 @@ class Element:
     """
 
     def __init__(self, tag, attrib={}, **extra):
-        if not isinstance(attrib, dict):
-            raise TypeError("attrib must be dict, not %s" % (
+        if not isinstance(attrib, (dict, frozendict)):
+            raise TypeError("attrib must be dict or frozendict, not %s" % (
                 attrib.__class__.__name__,))
         self.tag = tag
         self.attrib = {**attrib, **extra}
index f60a4c295e6495b4fdcd12a20e34e61f01016b2f..e0bc69c5fe22f83c4fed4ffe8061c027b7d9e7a0 100644 (file)
@@ -16,6 +16,7 @@
 #endif
 
 #include "Python.h"
+#include "pycore_dict.h"          // _PyDict_CopyAsDict()
 #include "pycore_pyhash.h"        // _Py_HashSecret
 #include "pycore_weakref.h"       // FT_CLEAR_WEAKREFS()
 
@@ -382,13 +383,14 @@ get_attrib_from_keywords(PyObject *kwds)
         /* If attrib was found in kwds, copy its value and remove it from
          * kwds
          */
-        if (!PyDict_Check(attrib)) {
-            PyErr_Format(PyExc_TypeError, "attrib must be dict, not %.100s",
-                         Py_TYPE(attrib)->tp_name);
+        if (!PyAnyDict_Check(attrib)) {
+            PyErr_Format(PyExc_TypeError,
+                         "attrib must be dict or frozendict, not %T",
+                         attrib);
             Py_DECREF(attrib);
             return NULL;
         }
-        Py_SETREF(attrib, PyDict_Copy(attrib));
+        Py_SETREF(attrib, _PyDict_CopyAsDict(attrib));
     }
     else {
         attrib = PyDict_New();
@@ -416,12 +418,18 @@ element_init(PyObject *self, PyObject *args, PyObject *kwds)
     PyObject *attrib = NULL;
     ElementObject *self_elem;
 
-    if (!PyArg_ParseTuple(args, "O|O!:Element", &tag, &PyDict_Type, &attrib))
+    if (!PyArg_ParseTuple(args, "O|O:Element", &tag, &attrib))
         return -1;
+    if (attrib != NULL && !PyAnyDict_Check(attrib)) {
+        PyErr_Format(PyExc_TypeError,
+                     "Element() argument 2 must be dict or frozendict, not %T",
+                     attrib);
+        return -1;
+    }
 
     if (attrib) {
         /* attrib passed as positional arg */
-        attrib = PyDict_Copy(attrib);
+        attrib = _PyDict_CopyAsDict(attrib);
         if (!attrib)
             return -1;
         if (kwds) {
@@ -2111,10 +2119,10 @@ static int
 element_attrib_setter(PyObject *op, PyObject *value, void *closure)
 {
     _VALIDATE_ATTR_VALUE(value);
-    if (!PyDict_Check(value)) {
+    if (!PyAnyDict_Check(value)) {
         PyErr_Format(PyExc_TypeError,
-                     "attrib must be dict, not %.200s",
-                     Py_TYPE(value)->tp_name);
+                     "attrib must be dict or frozendict, not %T",
+                     value);
         return -1;
     }
     ElementObject *self = _Element_CAST(op);