Skip to content

Commit a486d45

Browse files
pythongh-140601: Add ResourceWarning to iterparse when not closed (pythonGH-140603)
When iterparse() opens a file by filename and is not explicitly closed, emit a ResourceWarning to alert developers of the resource leak. Signed-off-by: Osama Abdelkader <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 209eaff commit a486d45

File tree

5 files changed

+69
-4
lines changed

5 files changed

+69
-4
lines changed

Doc/library/xml.etree.elementtree.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,10 @@ Functions
656656
.. versionchanged:: 3.13
657657
Added the :meth:`!close` method.
658658

659+
.. versionchanged:: next
660+
A :exc:`ResourceWarning` is now emitted if the iterator opened a file
661+
and is not explicitly closed.
662+
659663

660664
.. function:: parse(source, parser=None)
661665

Doc/whatsnew/3.15.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,3 +1244,9 @@ that may require changes to your code.
12441244

12451245
* :meth:`~mmap.mmap.resize` has been removed on platforms that don't support the
12461246
underlying syscall, instead of raising a :exc:`SystemError`.
1247+
1248+
* Resource warning is now emitted for unclosed
1249+
:func:`xml.etree.ElementTree.iterparse` iterator if it opened a file.
1250+
Use its :meth:`!close` method or the :func:`contextlib.closing` context
1251+
manager to close it.
1252+
(Contributed by Osama Abdelkader and Serhiy Storchaka in :gh:`140601`.)

Lib/test/test_xml_etree.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,18 +1436,40 @@ def test_nonexistent_file(self):
14361436

14371437
def test_resource_warnings_not_exhausted(self):
14381438
# Not exhausting the iterator still closes the underlying file (bpo-43292)
1439+
# Not closing before del should emit ResourceWarning
14391440
it = ET.iterparse(SIMPLE_XMLFILE)
14401441
with warnings_helper.check_no_resource_warning(self):
1442+
it.close()
1443+
del it
1444+
gc_collect()
1445+
1446+
it = ET.iterparse(SIMPLE_XMLFILE)
1447+
with self.assertWarns(ResourceWarning) as wm:
14411448
del it
14421449
gc_collect()
1450+
# Not 'unclosed file'.
1451+
self.assertIn('unclosed iterparse iterator', str(wm.warning))
1452+
self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
1453+
self.assertEqual(wm.filename, __file__)
14431454

14441455
it = ET.iterparse(SIMPLE_XMLFILE)
14451456
with warnings_helper.check_no_resource_warning(self):
14461457
action, elem = next(it)
1458+
it.close()
14471459
self.assertEqual((action, elem.tag), ('end', 'element'))
14481460
del it, elem
14491461
gc_collect()
14501462

1463+
it = ET.iterparse(SIMPLE_XMLFILE)
1464+
with self.assertWarns(ResourceWarning) as wm:
1465+
action, elem = next(it)
1466+
self.assertEqual((action, elem.tag), ('end', 'element'))
1467+
del it, elem
1468+
gc_collect()
1469+
self.assertIn('unclosed iterparse iterator', str(wm.warning))
1470+
self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
1471+
self.assertEqual(wm.filename, __file__)
1472+
14511473
def test_resource_warnings_failed_iteration(self):
14521474
self.addCleanup(os_helper.unlink, TESTFN)
14531475
with open(TESTFN, "wb") as f:
@@ -1461,15 +1483,40 @@ def test_resource_warnings_failed_iteration(self):
14611483
next(it)
14621484
self.assertEqual(str(cm.exception),
14631485
'junk after document element: line 1, column 12')
1486+
it.close()
14641487
del cm, it
14651488
gc_collect()
14661489

1490+
it = ET.iterparse(TESTFN)
1491+
action, elem = next(it)
1492+
self.assertEqual((action, elem.tag), ('end', 'document'))
1493+
with self.assertWarns(ResourceWarning) as wm:
1494+
with self.assertRaises(ET.ParseError) as cm:
1495+
next(it)
1496+
self.assertEqual(str(cm.exception),
1497+
'junk after document element: line 1, column 12')
1498+
del cm, it
1499+
gc_collect()
1500+
self.assertIn('unclosed iterparse iterator', str(wm.warning))
1501+
self.assertIn(repr(TESTFN), str(wm.warning))
1502+
self.assertEqual(wm.filename, __file__)
1503+
14671504
def test_resource_warnings_exhausted(self):
14681505
it = ET.iterparse(SIMPLE_XMLFILE)
14691506
with warnings_helper.check_no_resource_warning(self):
1507+
list(it)
1508+
it.close()
1509+
del it
1510+
gc_collect()
1511+
1512+
it = ET.iterparse(SIMPLE_XMLFILE)
1513+
with self.assertWarns(ResourceWarning) as wm:
14701514
list(it)
14711515
del it
14721516
gc_collect()
1517+
self.assertIn('unclosed iterparse iterator', str(wm.warning))
1518+
self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
1519+
self.assertEqual(wm.filename, __file__)
14731520

14741521
def test_close_not_exhausted(self):
14751522
iterparse = ET.iterparse

Lib/xml/etree/ElementTree.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,16 +1261,20 @@ def iterator(source):
12611261
gen = iterator(source)
12621262
class IterParseIterator(collections.abc.Iterator):
12631263
__next__ = gen.__next__
1264+
12641265
def close(self):
1266+
nonlocal close_source
12651267
if close_source:
12661268
source.close()
1269+
close_source = False
12671270
gen.close()
12681271

1269-
def __del__(self):
1270-
# TODO: Emit a ResourceWarning if it was not explicitly closed.
1271-
# (When the close() method will be supported in all maintained Python versions.)
1272+
def __del__(self, _warn=warnings.warn):
12721273
if close_source:
1273-
source.close()
1274+
try:
1275+
_warn(f"unclosed iterparse iterator {source.name!r}", ResourceWarning, stacklevel=2)
1276+
finally:
1277+
source.close()
12741278

12751279
it = IterParseIterator()
12761280
it.root = None
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`xml.etree.ElementTree.iterparse` now emits a :exc:`ResourceWarning`
2+
when the iterator is not explicitly closed and was opened with a filename.
3+
This helps developers identify and fix resource leaks. Patch by Osama
4+
Abdelkader.

0 commit comments

Comments
 (0)