summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-02-04 15:25:21 (GMT)
committerGitHub <noreply@github.com>2024-02-04 15:25:21 (GMT)
commitca715e56a13feabc15c368898df6511613d18987 (patch)
treebb2f617c9916aeadc54fd40384310d7f053d2bc3
parentfc060969117f5a5dc96c220eb91b1e2f863d71cf (diff)
downloadcpython-ca715e56a13feabc15c368898df6511613d18987.zip
cpython-ca715e56a13feabc15c368898df6511613d18987.tar.gz
cpython-ca715e56a13feabc15c368898df6511613d18987.tar.bz2
gh-69893: Add the close() method for xml.etree.ElementTree.iterparse() iterator (GH-114534)
-rw-r--r--Doc/library/xml.etree.elementtree.rst5
-rw-r--r--Doc/whatsnew/3.13.rst8
-rw-r--r--Lib/test/test_xml_etree.py85
-rw-r--r--Lib/xml/etree/ElementTree.py9
-rw-r--r--Misc/NEWS.d/next/Library/2024-01-24-17-25-18.gh-issue-69893.PQq5fR.rst2
5 files changed, 105 insertions, 4 deletions
diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst
index bb6773c..75a7915 100644
--- a/Doc/library/xml.etree.elementtree.rst
+++ b/Doc/library/xml.etree.elementtree.rst
@@ -625,6 +625,8 @@ Functions
target. Returns an :term:`iterator` providing ``(event, elem)`` pairs;
it has a ``root`` attribute that references the root element of the
resulting XML tree once *source* is fully read.
+ The iterator has the :meth:`!close` method that closes the internal
+ file object if *source* is a filename.
Note that while :func:`iterparse` builds the tree incrementally, it issues
blocking reads on *source* (or the file it names). As such, it's unsuitable
@@ -647,6 +649,9 @@ Functions
.. versionchanged:: 3.8
The ``comment`` and ``pi`` events were added.
+ .. versionchanged:: 3.13
+ Added the :meth:`!close` method.
+
.. function:: parse(source, parser=None)
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index f17c6ec..77f4fce 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -472,6 +472,14 @@ warnings
warning may also be emitted when a decorated function or class is used at runtime.
See :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.)
+xml.etree.ElementTree
+---------------------
+
+* Add the :meth:`!close` method for the iterator returned by
+ :func:`~xml.etree.ElementTree.iterparse` for explicit cleaning up.
+ (Contributed by Serhiy Storchaka in :gh:`69893`.)
+
+
Optimizations
=============
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 221545b..a435ec7 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -555,6 +555,17 @@ class ElementTreeTest(unittest.TestCase):
('end', '{namespace}root'),
])
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ context = iterparse(source)
+ action, elem = next(context)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ self.assertEqual([(action, elem.tag) for action, elem in context], [
+ ('end', 'element'),
+ ('end', 'empty-element'),
+ ('end', 'root'),
+ ])
+ self.assertEqual(context.root.tag, 'root')
+
events = ()
context = iterparse(SIMPLE_XMLFILE, events)
self.assertEqual([(action, elem.tag) for action, elem in context], [])
@@ -646,12 +657,81 @@ class ElementTreeTest(unittest.TestCase):
# Not exhausting the iterator still closes the resource (bpo-43292)
with warnings_helper.check_no_resource_warning(self):
- it = iterparse(TESTFN)
+ it = iterparse(SIMPLE_XMLFILE)
del it
+ with warnings_helper.check_no_resource_warning(self):
+ it = iterparse(SIMPLE_XMLFILE)
+ it.close()
+ del it
+
+ with warnings_helper.check_no_resource_warning(self):
+ it = iterparse(SIMPLE_XMLFILE)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ del it, elem
+
+ with warnings_helper.check_no_resource_warning(self):
+ it = iterparse(SIMPLE_XMLFILE)
+ action, elem = next(it)
+ it.close()
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ del it, elem
+
with self.assertRaises(FileNotFoundError):
iterparse("nonexistent")
+ def test_iterparse_close(self):
+ iterparse = ET.iterparse
+
+ it = iterparse(SIMPLE_XMLFILE)
+ it.close()
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = iterparse(source)
+ it.close()
+ self.assertFalse(source.closed)
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ it = iterparse(SIMPLE_XMLFILE)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ it.close()
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = iterparse(source)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ it.close()
+ self.assertFalse(source.closed)
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ it = iterparse(SIMPLE_XMLFILE)
+ list(it)
+ it.close()
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = iterparse(source)
+ list(it)
+ it.close()
+ self.assertFalse(source.closed)
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
def test_writefile(self):
elem = ET.Element("tag")
elem.text = "text"
@@ -3044,8 +3124,7 @@ class ElementIterTest(unittest.TestCase):
# With an explicit parser too (issue #9708)
sourcefile = serialize(doc, to_string=False)
parser = ET.XMLParser(target=ET.TreeBuilder())
- self.assertEqual(next(ET.iterparse(sourcefile, parser=parser))[0],
- 'end')
+ self.assertEqual(next(ET.iterparse(sourcefile, parser=parser))[0], 'end')
tree = ET.ElementTree(None)
self.assertRaises(AttributeError, tree.iter)
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index bb7362d..a37fead 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -1248,10 +1248,17 @@ def iterparse(source, events=None, parser=None):
if close_source:
source.close()
+ gen = iterator(source)
class IterParseIterator(collections.abc.Iterator):
- __next__ = iterator(source).__next__
+ __next__ = gen.__next__
+ def close(self):
+ if close_source:
+ source.close()
+ gen.close()
def __del__(self):
+ # TODO: Emit a ResourceWarning if it was not explicitly closed.
+ # (When the close() method will be supported in all maintained Python versions.)
if close_source:
source.close()
diff --git a/Misc/NEWS.d/next/Library/2024-01-24-17-25-18.gh-issue-69893.PQq5fR.rst b/Misc/NEWS.d/next/Library/2024-01-24-17-25-18.gh-issue-69893.PQq5fR.rst
new file mode 100644
index 0000000..1ebf434
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-01-24-17-25-18.gh-issue-69893.PQq5fR.rst
@@ -0,0 +1,2 @@
+Add the :meth:`!close` method for the iterator returned by
+:func:`xml.etree.ElementTree.iterparse`.