Skip to content

Commit 7bdfce0

Browse files
authored
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.
1 parent c0ecf21 commit 7bdfce0

File tree

5 files changed

+36
-14
lines changed

5 files changed

+36
-14
lines changed

Doc/library/xml.etree.elementtree.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,9 @@ Functions
702702
attributes. *extra* contains additional attributes, given as keyword
703703
arguments. Returns an element instance.
704704

705+
.. versionchanged:: next
706+
*attrib* can now be a :class:`frozendict`.
707+
705708

706709
.. function:: tostring(element, encoding="us-ascii", method="xml", *, \
707710
xml_declaration=None, default_namespace=None, \
@@ -887,6 +890,9 @@ Element Objects
887890
an optional dictionary, containing element attributes. *extra* contains
888891
additional attributes, given as keyword arguments.
889892

893+
.. versionchanged:: next
894+
*attrib* can now be a :class:`frozendict`.
895+
890896

891897
.. attribute:: tag
892898

Include/internal/pycore_dict.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ extern void _PyDict_Clear_LockHeld(PyObject *op);
160160
PyAPI_FUNC(void) _PyDict_EnsureSharedOnRead(PyDictObject *mp);
161161
#endif
162162

163-
extern PyObject* _PyDict_CopyAsDict(PyObject *op);
163+
// Export for '_elementtree' shared extension
164+
PyAPI_FUNC(PyObject*) _PyDict_CopyAsDict(PyObject *op);
164165

165166
#define DKIX_EMPTY (-1)
166167
#define DKIX_DUMMY (-2) /* Used internally */

Lib/test/test_xml_etree.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4472,17 +4472,24 @@ def test_issue14818(self):
44724472
ET.Element('a', dict(href="#"), id="foo"),
44734473
ET.Element('a', href="#", id="foo"),
44744474
ET.Element('a', dict(href="#", id="foo"), href="#", id="foo"),
4475+
ET.Element('a', frozendict(href="#", id="foo")),
4476+
ET.Element('a', frozendict(href="#"), id="foo"),
4477+
ET.Element('a', attrib=frozendict(href="#", id="foo")),
44754478
]
44764479
for e in elements:
44774480
self.assertEqual(e.tag, 'a')
44784481
self.assertEqual(e.attrib, dict(href="#", id="foo"))
44794482

44804483
e2 = ET.SubElement(elements[0], 'foobar', attrib={'key1': 'value1'})
44814484
self.assertEqual(e2.attrib['key1'], 'value1')
4485+
e3 = ET.SubElement(elements[0], 'foobar',
4486+
attrib=frozendict({'key1': 'value1'}))
4487+
self.assertEqual(e3.attrib['key1'], 'value1')
44824488

4483-
with self.assertRaisesRegex(TypeError, 'must be dict, not str'):
4489+
errmsg = 'must be dict or frozendict, not str'
4490+
with self.assertRaisesRegex(TypeError, errmsg):
44844491
ET.Element('a', "I'm not a dict")
4485-
with self.assertRaisesRegex(TypeError, 'must be dict, not str'):
4492+
with self.assertRaisesRegex(TypeError, errmsg):
44864493
ET.Element('a', attrib="I'm not a dict")
44874494

44884495
# --------------------------------------------------------------------

Lib/xml/etree/ElementTree.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ class Element:
165165
"""
166166

167167
def __init__(self, tag, attrib={}, **extra):
168-
if not isinstance(attrib, dict):
169-
raise TypeError("attrib must be dict, not %s" % (
168+
if not isinstance(attrib, (dict, frozendict)):
169+
raise TypeError("attrib must be dict or frozendict, not %s" % (
170170
attrib.__class__.__name__,))
171171
self.tag = tag
172172
self.attrib = {**attrib, **extra}

Modules/_elementtree.c

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#endif
1717

1818
#include "Python.h"
19+
#include "pycore_dict.h" // _PyDict_CopyAsDict()
1920
#include "pycore_pyhash.h" // _Py_HashSecret
2021
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
2122

@@ -382,13 +383,14 @@ get_attrib_from_keywords(PyObject *kwds)
382383
/* If attrib was found in kwds, copy its value and remove it from
383384
* kwds
384385
*/
385-
if (!PyDict_Check(attrib)) {
386-
PyErr_Format(PyExc_TypeError, "attrib must be dict, not %.100s",
387-
Py_TYPE(attrib)->tp_name);
386+
if (!PyAnyDict_Check(attrib)) {
387+
PyErr_Format(PyExc_TypeError,
388+
"attrib must be dict or frozendict, not %T",
389+
attrib);
388390
Py_DECREF(attrib);
389391
return NULL;
390392
}
391-
Py_SETREF(attrib, PyDict_Copy(attrib));
393+
Py_SETREF(attrib, _PyDict_CopyAsDict(attrib));
392394
}
393395
else {
394396
attrib = PyDict_New();
@@ -416,12 +418,18 @@ element_init(PyObject *self, PyObject *args, PyObject *kwds)
416418
PyObject *attrib = NULL;
417419
ElementObject *self_elem;
418420

419-
if (!PyArg_ParseTuple(args, "O|O!:Element", &tag, &PyDict_Type, &attrib))
421+
if (!PyArg_ParseTuple(args, "O|O:Element", &tag, &attrib))
420422
return -1;
423+
if (attrib != NULL && !PyAnyDict_Check(attrib)) {
424+
PyErr_Format(PyExc_TypeError,
425+
"Element() argument 2 must be dict or frozendict, not %T",
426+
attrib);
427+
return -1;
428+
}
421429

422430
if (attrib) {
423431
/* attrib passed as positional arg */
424-
attrib = PyDict_Copy(attrib);
432+
attrib = _PyDict_CopyAsDict(attrib);
425433
if (!attrib)
426434
return -1;
427435
if (kwds) {
@@ -2111,10 +2119,10 @@ static int
21112119
element_attrib_setter(PyObject *op, PyObject *value, void *closure)
21122120
{
21132121
_VALIDATE_ATTR_VALUE(value);
2114-
if (!PyDict_Check(value)) {
2122+
if (!PyAnyDict_Check(value)) {
21152123
PyErr_Format(PyExc_TypeError,
2116-
"attrib must be dict, not %.200s",
2117-
Py_TYPE(value)->tp_name);
2124+
"attrib must be dict or frozendict, not %T",
2125+
value);
21182126
return -1;
21192127
}
21202128
ElementObject *self = _Element_CAST(op);

0 commit comments

Comments
 (0)