Skip to content

Commit d072962

Browse files
committed
bpo-32424: Synchronize xml.etree.ElementTree.Element copy methods.
The Python implementation had Element.copy() while the C implementation had Element.__copy__() and Element.__deepcopy__(). This synchronizes the two implementations and deprecates elem.copy() in favor of copy.copy(elem).
1 parent e5f7dcc commit d072962

File tree

4 files changed

+239
-31
lines changed

4 files changed

+239
-31
lines changed

Lib/test/test_xml_etree.py

Lines changed: 176 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# monkey-patched when running the "test_xml_etree_c" test suite.
77

88
import copy
9+
import itertools
910
import functools
1011
import html
1112
import io
@@ -102,6 +103,11 @@ def newtest(*args, **kwargs):
102103
return decorator
103104

104105

106+
#-----------------------------------------------------------------------------
107+
# UNIT TESTS
108+
#-----------------------------------------------------------------------------
109+
110+
105111
class ModuleTest(unittest.TestCase):
106112
def test_sanity(self):
107113
# Import sanity.
@@ -115,6 +121,143 @@ def test_all(self):
115121
support.check__all__(self, ET, names, blacklist=("HTML_EMPTY",))
116122

117123

124+
class ElementTree_Element_UnitTest(unittest.TestCase):
125+
126+
def test___init__(self):
127+
tag = "foo"
128+
attrib = { "zix": "wyp" }
129+
130+
element_foo = ET.Element(tag, attrib)
131+
132+
with self.subTest("traits of an element"):
133+
self.assertIsInstance(element_foo, ET.Element)
134+
self.assertIn("tag", dir(element_foo))
135+
self.assertIn("attrib", dir(element_foo))
136+
self.assertIn("text", dir(element_foo))
137+
self.assertIn("tail", dir(element_foo))
138+
139+
with self.subTest("string attributes have expected values"):
140+
self.assertEqual(element_foo.tag, tag)
141+
self.assertIsNone(element_foo.text)
142+
self.assertIsNone(element_foo.tail)
143+
144+
with self.subTest("attrib is a copy"):
145+
self.assertIsNot(element_foo.attrib, attrib)
146+
self.assertEqual(element_foo.attrib, attrib)
147+
148+
with self.subTest("attrib isn't linked"):
149+
attrib["bar"] = "baz"
150+
151+
self.assertIsNot(element_foo.attrib, attrib)
152+
self.assertNotEqual(element_foo.attrib, attrib)
153+
154+
def test_copy(self):
155+
element_foo = ET.Element("foo", { "zix": "wyp" })
156+
element_foo.append(ET.Element("bar", { "baz": "qix" }))
157+
158+
with self.assertWarns(DeprecationWarning):
159+
element_foo2 = element_foo.copy()
160+
161+
with self.subTest("elements are not the same"):
162+
self.assertIsNot(element_foo2, element_foo)
163+
164+
with self.subTest("string attributes are the same"):
165+
self.assertIs(element_foo2.tag, element_foo.tag)
166+
self.assertIs(element_foo2.text, element_foo.text)
167+
self.assertIs(element_foo2.tail, element_foo.tail)
168+
169+
with self.subTest("string attributes are equal"):
170+
self.assertEqual(element_foo2.tag, element_foo.tag)
171+
self.assertEqual(element_foo2.text, element_foo.text)
172+
self.assertEqual(element_foo2.tail, element_foo.tail)
173+
174+
with self.subTest("children are the same"):
175+
for (child1, child2) in itertools.zip_longest(element_foo, element_foo2):
176+
self.assertIs(child1, child2)
177+
178+
with self.subTest("attrib is a copy"):
179+
self.assertIsNot(element_foo2.attrib, element_foo.attrib)
180+
self.assertEqual(element_foo2.attrib, element_foo.attrib)
181+
182+
with self.subTest("attrib isn't linked"):
183+
element_foo.attrib["bar"] = "baz"
184+
185+
self.assertIsNot(element_foo2.attrib, element_foo.attrib)
186+
self.assertNotEqual(element_foo2.attrib, element_foo.attrib)
187+
188+
def test___copy__(self):
189+
element_foo = ET.Element("foo", { "zix": "wyp" })
190+
element_foo.append(ET.Element("bar", { "baz": "qix" }))
191+
192+
element_foo2 = copy.copy(element_foo)
193+
194+
with self.subTest("elements are not the same"):
195+
self.assertIsNot(element_foo2, element_foo)
196+
197+
with self.subTest("string attributes are the same"):
198+
self.assertIs(element_foo2.tag, element_foo.tag)
199+
self.assertIs(element_foo2.text, element_foo.text)
200+
self.assertIs(element_foo2.tail, element_foo.tail)
201+
202+
with self.subTest("string attributes are equal"):
203+
self.assertEqual(element_foo2.tag, element_foo.tag)
204+
self.assertEqual(element_foo2.text, element_foo.text)
205+
self.assertEqual(element_foo2.tail, element_foo.tail)
206+
207+
with self.subTest("children are the same"):
208+
for (child1, child2) in itertools.zip_longest(element_foo, element_foo2):
209+
self.assertIs(child1, child2)
210+
211+
with self.subTest("attrib is a copy"):
212+
self.assertIsNot(element_foo2.attrib, element_foo.attrib)
213+
self.assertEqual(element_foo2.attrib, element_foo.attrib)
214+
215+
with self.subTest("attrib isn't linked"):
216+
element_foo.attrib["bar"] = "baz"
217+
218+
self.assertIsNot(element_foo2.attrib, element_foo.attrib)
219+
self.assertNotEqual(element_foo2.attrib, element_foo.attrib)
220+
221+
def test___deepcopy__(self):
222+
element_foo = ET.Element("foo", { "zix": "wyp" })
223+
element_foo.append(ET.Element("bar", { "baz": "qix" }))
224+
225+
element_foo2 = copy.deepcopy(element_foo)
226+
227+
with self.subTest("elements are not the same"):
228+
self.assertIsNot(element_foo2, element_foo)
229+
230+
# Strings should still be the same in a deep copy.
231+
with self.subTest("string attributes are the same"):
232+
self.assertIs(element_foo2.tag, element_foo.tag)
233+
self.assertIs(element_foo2.text, element_foo.text)
234+
self.assertIs(element_foo2.tail, element_foo.tail)
235+
236+
with self.subTest("string attributes are equal"):
237+
self.assertEqual(element_foo2.tag, element_foo.tag)
238+
self.assertEqual(element_foo2.text, element_foo.text)
239+
self.assertEqual(element_foo2.tail, element_foo.tail)
240+
241+
with self.subTest("children are not the same"):
242+
for (child1, child2) in itertools.zip_longest(element_foo, element_foo2):
243+
self.assertIsNot(child1, child2)
244+
245+
with self.subTest("attrib is a copy"):
246+
self.assertIsNot(element_foo2.attrib, element_foo.attrib)
247+
self.assertEqual(element_foo2.attrib, element_foo.attrib)
248+
249+
with self.subTest("attrib isn't linked"):
250+
element_foo.attrib["bar"] = "baz"
251+
252+
self.assertIsNot(element_foo2.attrib, element_foo.attrib)
253+
self.assertNotEqual(element_foo2.attrib, element_foo.attrib)
254+
255+
256+
#-----------------------------------------------------------------------------
257+
# INTEGRATION TESTS
258+
#-----------------------------------------------------------------------------
259+
260+
118261
def serialize(elem, to_string=True, encoding='unicode', **options):
119262
if encoding != 'unicode':
120263
file = io.BytesIO()
@@ -1786,36 +1929,38 @@ def test_cyclic_gc(self):
17861929
class Dummy:
17871930
pass
17881931

1789-
# Test the shortest cycle: d->element->d
1790-
d = Dummy()
1791-
d.dummyref = ET.Element('joe', attr=d)
1792-
wref = weakref.ref(d)
1793-
del d
1794-
gc_collect()
1795-
self.assertIsNone(wref())
1796-
1797-
# A longer cycle: d->e->e2->d
1798-
e = ET.Element('joe')
1799-
d = Dummy()
1800-
d.dummyref = e
1801-
wref = weakref.ref(d)
1802-
e2 = ET.SubElement(e, 'foo', attr=d)
1803-
del d, e, e2
1804-
gc_collect()
1805-
self.assertIsNone(wref())
1806-
1807-
# A cycle between Element objects as children of one another
1808-
# e1->e2->e3->e1
1809-
e1 = ET.Element('e1')
1810-
e2 = ET.Element('e2')
1811-
e3 = ET.Element('e3')
1812-
e1.append(e2)
1813-
e2.append(e2)
1814-
e3.append(e1)
1815-
wref = weakref.ref(e1)
1816-
del e1, e2, e3
1817-
gc_collect()
1818-
self.assertIsNone(wref())
1932+
with self.subTest("Test the shortest cycle: d->element->d"):
1933+
d = Dummy()
1934+
d.dummyref = ET.Element('joe', attr=d)
1935+
wref = weakref.ref(d)
1936+
del d
1937+
gc_collect()
1938+
self.assertIsNone(wref())
1939+
1940+
with self.subTest("A longer cycle: d->e->e2->d"):
1941+
e = ET.Element('joe')
1942+
d = Dummy()
1943+
d.dummyref = e
1944+
wref = weakref.ref(d)
1945+
e2 = ET.SubElement(e, 'foo', attr=d)
1946+
del d, e, e2
1947+
gc_collect()
1948+
self.assertIsNone(wref())
1949+
1950+
with self.subTest(
1951+
"A cycle between Element objects as children of one another: "
1952+
"e1->e2->e3->e1"
1953+
):
1954+
e1 = ET.Element('e1')
1955+
e2 = ET.Element('e2')
1956+
e3 = ET.Element('e3')
1957+
e3.append(e1)
1958+
e2.append(e3)
1959+
e1.append(e2)
1960+
wref = weakref.ref(e1)
1961+
del e1, e2, e3
1962+
gc_collect()
1963+
self.assertIsNone(wref())
18191964

18201965
def test_weakref(self):
18211966
flag = False
@@ -3143,6 +3288,7 @@ def test_main(module=None):
31433288

31443289
test_classes = [
31453290
ModuleTest,
3291+
ElementTree_Element_UnitTest,
31463292
ElementSlicingTest,
31473293
BasicElementTest,
31483294
BadElementTest,

Lib/xml/etree/ElementTree.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
import sys
9595
import re
9696
import warnings
97+
import copy
9798
import io
9899
import collections
99100
import collections.abc
@@ -196,12 +197,35 @@ def copy(self):
196197
original tree.
197198
198199
"""
200+
warnings.warn(
201+
"elem.copy() is deprecated. Use copy.copy(elem) instead.",
202+
DeprecationWarning
203+
)
204+
return self.__copy__()
205+
206+
def __copy__(self):
199207
elem = self.makeelement(self.tag, self.attrib)
200208
elem.text = self.text
201209
elem.tail = self.tail
202210
elem[:] = self
203211
return elem
204212

213+
def __deepcopy__(self, memo):
214+
tag = copy.deepcopy(self.tag, memo)
215+
attrib = copy.deepcopy(self.attrib, memo)
216+
217+
elem = self.makeelement(tag, attrib)
218+
219+
elem.text = copy.deepcopy(self.text, memo)
220+
elem.tail = copy.deepcopy(self.tail, memo)
221+
222+
for child in self:
223+
elem.append(copy.deepcopy(child, memo))
224+
225+
memo[id(self)] = elem
226+
227+
return elem
228+
205229
def __len__(self):
206230
return len(self._children)
207231

Modules/_elementtree.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,12 @@ create_new_element(PyObject* tag, PyObject* attrib)
283283
PyObject_GC_Track(self);
284284

285285
if (attrib != Py_None && !is_empty_dict(attrib)) {
286+
attrib = PyDict_Copy(attrib);
286287
if (create_extra(self, attrib) < 0) {
287288
Py_DECREF(self);
288289
return NULL;
289290
}
291+
Py_DECREF(attrib);
290292
}
291293

292294
return (PyObject*) self;
@@ -725,6 +727,24 @@ _elementtree_Element___copy___impl(ElementObject *self)
725727
return (PyObject*) element;
726728
}
727729

730+
/*[clinic input]
731+
_elementtree.Element.copy
732+
733+
[clinic start generated code]*/
734+
735+
static PyObject *
736+
_elementtree_Element_copy_impl(ElementObject *self)
737+
/*[clinic end generated code: output=84660e8524276b22 input=1f8134305a7719a3]*/
738+
{
739+
if (PyErr_WarnEx(PyExc_DeprecationWarning,
740+
"elem.copy() is deprecated. Use copy.copy(elem) instead.",
741+
1) < 0) {
742+
return NULL;
743+
}
744+
745+
return _elementtree_Element___copy___impl(self);
746+
}
747+
728748
/* Helper for a deep copy. */
729749
LOCAL(PyObject *) deepcopy(PyObject *, PyObject *);
730750

@@ -3791,6 +3811,7 @@ static PyMethodDef element_methods[] = {
37913811

37923812
_ELEMENTTREE_ELEMENT_MAKEELEMENT_METHODDEF
37933813

3814+
_ELEMENTTREE_ELEMENT_COPY_METHODDEF
37943815
_ELEMENTTREE_ELEMENT___COPY___METHODDEF
37953816
_ELEMENTTREE_ELEMENT___DEEPCOPY___METHODDEF
37963817
_ELEMENTTREE_ELEMENT___SIZEOF___METHODDEF

Modules/clinic/_elementtree.c.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ _elementtree_Element___copy__(ElementObject *self, PyObject *Py_UNUSED(ignored))
6262
return _elementtree_Element___copy___impl(self);
6363
}
6464

65+
PyDoc_STRVAR(_elementtree_Element_copy__doc__,
66+
"copy($self, /)\n"
67+
"--\n"
68+
"\n");
69+
70+
#define _ELEMENTTREE_ELEMENT_COPY_METHODDEF \
71+
{"copy", (PyCFunction)_elementtree_Element_copy, METH_NOARGS, _elementtree_Element_copy__doc__},
72+
73+
static PyObject *
74+
_elementtree_Element_copy_impl(ElementObject *self);
75+
76+
static PyObject *
77+
_elementtree_Element_copy(ElementObject *self, PyObject *Py_UNUSED(ignored))
78+
{
79+
return _elementtree_Element_copy_impl(self);
80+
}
81+
6582
PyDoc_STRVAR(_elementtree_Element___deepcopy____doc__,
6683
"__deepcopy__($self, memo, /)\n"
6784
"--\n"
@@ -749,4 +766,4 @@ _elementtree_XMLParser__setevents(XMLParserObject *self, PyObject *const *args,
749766
exit:
750767
return return_value;
751768
}
752-
/*[clinic end generated code: output=c5a85a88bbb5cc06 input=a9049054013a1b77]*/
769+
/*[clinic end generated code: output=1d2dc2ddb03bcaeb input=a9049054013a1b77]*/

0 commit comments

Comments
 (0)