gh-140601: Add ResourceWarning to iterparse when not closed (GH-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 <osama.abdelkader@gmail.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Osama Abdelkader 2025-11-13 20:05:28 +01:00 committed by GitHub
parent 209eaff68c
commit a486d452c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 4 deletions

View file

@ -656,6 +656,10 @@ Functions
.. versionchanged:: 3.13
Added the :meth:`!close` method.
.. versionchanged:: next
A :exc:`ResourceWarning` is now emitted if the iterator opened a file
and is not explicitly closed.
.. function:: parse(source, parser=None)

View file

@ -1244,3 +1244,9 @@ that may require changes to your code.
* :meth:`~mmap.mmap.resize` has been removed on platforms that don't support the
underlying syscall, instead of raising a :exc:`SystemError`.
* Resource warning is now emitted for unclosed
:func:`xml.etree.ElementTree.iterparse` iterator if it opened a file.
Use its :meth:`!close` method or the :func:`contextlib.closing` context
manager to close it.
(Contributed by Osama Abdelkader and Serhiy Storchaka in :gh:`140601`.)

View file

@ -1436,17 +1436,39 @@ def test_nonexistent_file(self):
def test_resource_warnings_not_exhausted(self):
# Not exhausting the iterator still closes the underlying file (bpo-43292)
# Not closing before del should emit ResourceWarning
it = ET.iterparse(SIMPLE_XMLFILE)
with warnings_helper.check_no_resource_warning(self):
it.close()
del it
gc_collect()
it = ET.iterparse(SIMPLE_XMLFILE)
with self.assertWarns(ResourceWarning) as wm:
del it
gc_collect()
# Not 'unclosed file'.
self.assertIn('unclosed iterparse iterator', str(wm.warning))
self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
self.assertEqual(wm.filename, __file__)
it = ET.iterparse(SIMPLE_XMLFILE)
with warnings_helper.check_no_resource_warning(self):
action, elem = next(it)
it.close()
self.assertEqual((action, elem.tag), ('end', 'element'))
del it, elem
gc_collect()
it = ET.iterparse(SIMPLE_XMLFILE)
with self.assertWarns(ResourceWarning) as wm:
action, elem = next(it)
self.assertEqual((action, elem.tag), ('end', 'element'))
del it, elem
gc_collect()
self.assertIn('unclosed iterparse iterator', str(wm.warning))
self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
self.assertEqual(wm.filename, __file__)
def test_resource_warnings_failed_iteration(self):
self.addCleanup(os_helper.unlink, TESTFN)
@ -1461,16 +1483,41 @@ def test_resource_warnings_failed_iteration(self):
next(it)
self.assertEqual(str(cm.exception),
'junk after document element: line 1, column 12')
it.close()
del cm, it
gc_collect()
it = ET.iterparse(TESTFN)
action, elem = next(it)
self.assertEqual((action, elem.tag), ('end', 'document'))
with self.assertWarns(ResourceWarning) as wm:
with self.assertRaises(ET.ParseError) as cm:
next(it)
self.assertEqual(str(cm.exception),
'junk after document element: line 1, column 12')
del cm, it
gc_collect()
self.assertIn('unclosed iterparse iterator', str(wm.warning))
self.assertIn(repr(TESTFN), str(wm.warning))
self.assertEqual(wm.filename, __file__)
def test_resource_warnings_exhausted(self):
it = ET.iterparse(SIMPLE_XMLFILE)
with warnings_helper.check_no_resource_warning(self):
list(it)
it.close()
del it
gc_collect()
it = ET.iterparse(SIMPLE_XMLFILE)
with self.assertWarns(ResourceWarning) as wm:
list(it)
del it
gc_collect()
self.assertIn('unclosed iterparse iterator', str(wm.warning))
self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
self.assertEqual(wm.filename, __file__)
def test_close_not_exhausted(self):
iterparse = ET.iterparse

View file

@ -1261,16 +1261,20 @@ def iterator(source):
gen = iterator(source)
class IterParseIterator(collections.abc.Iterator):
__next__ = gen.__next__
def close(self):
nonlocal close_source
if close_source:
source.close()
close_source = False
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.)
def __del__(self, _warn=warnings.warn):
if close_source:
source.close()
try:
_warn(f"unclosed iterparse iterator {source.name!r}", ResourceWarning, stacklevel=2)
finally:
source.close()
it = IterParseIterator()
it.root = None

View file

@ -0,0 +1,4 @@
:func:`xml.etree.ElementTree.iterparse` now emits a :exc:`ResourceWarning`
when the iterator is not explicitly closed and was opened with a filename.
This helps developers identify and fix resource leaks. Patch by Osama
Abdelkader.