Skip to content

Commit ca715e5

Browse files
gh-69893: Add the close() method for xml.etree.ElementTree.iterparse() iterator (GH-114534)
1 parent fc06096 commit ca715e5

File tree

5 files changed

+105
-4
lines changed

5 files changed

+105
-4
lines changed

Doc/library/xml.etree.elementtree.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,8 @@ Functions
625625
target. Returns an :term:`iterator` providing ``(event, elem)`` pairs;
626626
it has a ``root`` attribute that references the root element of the
627627
resulting XML tree once *source* is fully read.
628+
The iterator has the :meth:`!close` method that closes the internal
629+
file object if *source* is a filename.
628630

629631
Note that while :func:`iterparse` builds the tree incrementally, it issues
630632
blocking reads on *source* (or the file it names). As such, it's unsuitable
@@ -647,6 +649,9 @@ Functions
647649
.. versionchanged:: 3.8
648650
The ``comment`` and ``pi`` events were added.
649651

652+
.. versionchanged:: 3.13
653+
Added the :meth:`!close` method.
654+
650655

651656
.. function:: parse(source, parser=None)
652657

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,14 @@ warnings
472472
warning may also be emitted when a decorated function or class is used at runtime.
473473
See :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.)
474474

475+
xml.etree.ElementTree
476+
---------------------
477+
478+
* Add the :meth:`!close` method for the iterator returned by
479+
:func:`~xml.etree.ElementTree.iterparse` for explicit cleaning up.
480+
(Contributed by Serhiy Storchaka in :gh:`69893`.)
481+
482+
475483
Optimizations
476484
=============
477485

Lib/test/test_xml_etree.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,17 @@ def test_iterparse(self):
555555
('end', '{namespace}root'),
556556
])
557557

558+
with open(SIMPLE_XMLFILE, 'rb') as source:
559+
context = iterparse(source)
560+
action, elem = next(context)
561+
self.assertEqual((action, elem.tag), ('end', 'element'))
562+
self.assertEqual([(action, elem.tag) for action, elem in context], [
563+
('end', 'element'),
564+
('end', 'empty-element'),
565+
('end', 'root'),
566+
])
567+
self.assertEqual(context.root.tag, 'root')
568+
558569
events = ()
559570
context = iterparse(SIMPLE_XMLFILE, events)
560571
self.assertEqual([(action, elem.tag) for action, elem in context], [])
@@ -646,12 +657,81 @@ def test_iterparse(self):
646657

647658
# Not exhausting the iterator still closes the resource (bpo-43292)
648659
with warnings_helper.check_no_resource_warning(self):
649-
it = iterparse(TESTFN)
660+
it = iterparse(SIMPLE_XMLFILE)
650661
del it
651662

663+
with warnings_helper.check_no_resource_warning(self):
664+
it = iterparse(SIMPLE_XMLFILE)
665+
it.close()
666+
del it
667+
668+
with warnings_helper.check_no_resource_warning(self):
669+
it = iterparse(SIMPLE_XMLFILE)
670+
action, elem = next(it)
671+
self.assertEqual((action, elem.tag), ('end', 'element'))
672+
del it, elem
673+
674+
with warnings_helper.check_no_resource_warning(self):
675+
it = iterparse(SIMPLE_XMLFILE)
676+
action, elem = next(it)
677+
it.close()
678+
self.assertEqual((action, elem.tag), ('end', 'element'))
679+
del it, elem
680+
652681
with self.assertRaises(FileNotFoundError):
653682
iterparse("nonexistent")
654683

684+
def test_iterparse_close(self):
685+
iterparse = ET.iterparse
686+
687+
it = iterparse(SIMPLE_XMLFILE)
688+
it.close()
689+
with self.assertRaises(StopIteration):
690+
next(it)
691+
it.close() # idempotent
692+
693+
with open(SIMPLE_XMLFILE, 'rb') as source:
694+
it = iterparse(source)
695+
it.close()
696+
self.assertFalse(source.closed)
697+
with self.assertRaises(StopIteration):
698+
next(it)
699+
it.close() # idempotent
700+
701+
it = iterparse(SIMPLE_XMLFILE)
702+
action, elem = next(it)
703+
self.assertEqual((action, elem.tag), ('end', 'element'))
704+
it.close()
705+
with self.assertRaises(StopIteration):
706+
next(it)
707+
it.close() # idempotent
708+
709+
with open(SIMPLE_XMLFILE, 'rb') as source:
710+
it = iterparse(source)
711+
action, elem = next(it)
712+
self.assertEqual((action, elem.tag), ('end', 'element'))
713+
it.close()
714+
self.assertFalse(source.closed)
715+
with self.assertRaises(StopIteration):
716+
next(it)
717+
it.close() # idempotent
718+
719+
it = iterparse(SIMPLE_XMLFILE)
720+
list(it)
721+
it.close()
722+
with self.assertRaises(StopIteration):
723+
next(it)
724+
it.close() # idempotent
725+
726+
with open(SIMPLE_XMLFILE, 'rb') as source:
727+
it = iterparse(source)
728+
list(it)
729+
it.close()
730+
self.assertFalse(source.closed)
731+
with self.assertRaises(StopIteration):
732+
next(it)
733+
it.close() # idempotent
734+
655735
def test_writefile(self):
656736
elem = ET.Element("tag")
657737
elem.text = "text"
@@ -3044,8 +3124,7 @@ def test_basic(self):
30443124
# With an explicit parser too (issue #9708)
30453125
sourcefile = serialize(doc, to_string=False)
30463126
parser = ET.XMLParser(target=ET.TreeBuilder())
3047-
self.assertEqual(next(ET.iterparse(sourcefile, parser=parser))[0],
3048-
'end')
3127+
self.assertEqual(next(ET.iterparse(sourcefile, parser=parser))[0], 'end')
30493128

30503129
tree = ET.ElementTree(None)
30513130
self.assertRaises(AttributeError, tree.iter)

Lib/xml/etree/ElementTree.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1248,10 +1248,17 @@ def iterator(source):
12481248
if close_source:
12491249
source.close()
12501250

1251+
gen = iterator(source)
12511252
class IterParseIterator(collections.abc.Iterator):
1252-
__next__ = iterator(source).__next__
1253+
__next__ = gen.__next__
1254+
def close(self):
1255+
if close_source:
1256+
source.close()
1257+
gen.close()
12531258

12541259
def __del__(self):
1260+
# TODO: Emit a ResourceWarning if it was not explicitly closed.
1261+
# (When the close() method will be supported in all maintained Python versions.)
12551262
if close_source:
12561263
source.close()
12571264

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add the :meth:`!close` method for the iterator returned by
2+
:func:`xml.etree.ElementTree.iterparse`.

0 commit comments

Comments
 (0)