Skip to content

Commit

Permalink
pythongh-69893: Add the close() method for xml.etree.ElementTree.iter…
Browse files Browse the repository at this point in the history
…parse() iterator (pythonGH-114534)
  • Loading branch information
serhiy-storchaka authored and fsc-eriker committed Feb 14, 2024
1 parent 16baf0e commit ba4a301
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 4 deletions.
5 changes: 5 additions & 0 deletions Doc/library/xml.etree.elementtree.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=============

Expand Down
85 changes: 82 additions & 3 deletions Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,17 @@ def test_iterparse(self):
('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], [])
Expand Down Expand Up @@ -646,12 +657,81 @@ def test_iterparse(self):

# 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"
Expand Down Expand Up @@ -3044,8 +3124,7 @@ def test_basic(self):
# 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)
Expand Down
9 changes: 8 additions & 1 deletion Lib/xml/etree/ElementTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,10 +1248,17 @@ def iterator(source):
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()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add the :meth:`!close` method for the iterator returned by
:func:`xml.etree.ElementTree.iterparse`.

0 comments on commit ba4a301

Please sign in to comment.