https://github.com/python/cpython/commit/7bdfce0d3a1696ed08ffa16e4574428ef09a6b40
commit: 7bdfce0d3a1696ed08ffa16e4574428ef09a6b40
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-03-05T12:55:28+01:00
summary:
gh-145056: Accept frozendict in xml.etree (#145508)
Element and SubElement of xml.etree.ElementTree now also accept
frozendict for attrib.
Export _PyDict_CopyAsDict() function.
files:
M Doc/library/xml.etree.elementtree.rst
M Include/internal/pycore_dict.h
M Lib/test/test_xml_etree.py
M Lib/xml/etree/ElementTree.py
M Modules/_elementtree.c
diff --git a/Doc/library/xml.etree.elementtree.rst
b/Doc/library/xml.etree.elementtree.rst
index e021a81d2a2b87..45abf5b1e736b3 100644
--- a/Doc/library/xml.etree.elementtree.rst
+++ b/Doc/library/xml.etree.elementtree.rst
@@ -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
diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h
index a2c5ee41c37784..6d7d68eda84c5a 100644
--- a/Include/internal/pycore_dict.h
+++ b/Include/internal/pycore_dict.h
@@ -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 */
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 93162f52ba0344..5b06e422672b1d 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -4472,6 +4472,9 @@ def test_issue14818(self):
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 @@ def test_issue14818(self):
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")
# --------------------------------------------------------------------
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index e3d81a2c4560d9..57c5b64ea3ba70 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -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}
diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c
index f60a4c295e6495..e0bc69c5fe22f8 100644
--- a/Modules/_elementtree.c
+++ b/Modules/_elementtree.c
@@ -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);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]