Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-140601: Add ResourceWarning to iterparse when not closed
When iterparse() opens a file by filename and is not explicitly closed,
emit a ResourceWarning to alert developers of the resource leak.

This implements the TODO comment at line 1270 of ElementTree.py which
has been requesting this feature since the close() method was added.

- Add _closed flag to IterParseIterator to track state
- Emit ResourceWarning in __del__ if not closed
- Add comprehensive test cases
- Update existing tests to properly close iterators

Signed-off-by: Osama Abdelkader <osama.abdelkader@gmail.com>
  • Loading branch information
osamakader committed Oct 25, 2025
commit bf3f8888b277817bb674b899c564cef27cfd0a12
90 changes: 75 additions & 15 deletions Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,28 +692,31 @@ def test_iterparse(self):
it = iterparse(TESTFN)
action, elem = next(it)
self.assertEqual((action, elem.tag), ('end', 'document'))
with warnings_helper.check_no_resource_warning(self):
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
with self.assertRaises(ET.ParseError) as cm:
next(it)
self.assertEqual(str(cm.exception),
'junk after document element: line 1, column 12')
it.close() # Close to avoid ResourceWarning
del cm, it

# Not exhausting the iterator still closes the resource (bpo-43292)
with warnings_helper.check_no_resource_warning(self):
it = iterparse(SIMPLE_XMLFILE)
del it
# Deleting iterator without close() should emit ResourceWarning (bpo-43292)
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated
it = iterparse(SIMPLE_XMLFILE)
del it
import gc
gc.collect() # Ensure previous iterator is cleaned up
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to use support.gc_collect() instead of directly calling gc.collect() was specifically added for use in tests to ensure consistent and deterministic garbage collection behavior across platforms and configurations.


# Explicitly calling close() should not emit warning
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
# Not closing before del should emit ResourceWarning
it = iterparse(SIMPLE_XMLFILE)
action, elem = next(it)
self.assertEqual((action, elem.tag), ('end', 'element'))
del it, elem
gc.collect() # Ensure previous iterator is cleaned up

with warnings_helper.check_no_resource_warning(self):
it = iterparse(SIMPLE_XMLFILE)
Expand All @@ -725,6 +728,63 @@ def test_iterparse(self):
with self.assertRaises(FileNotFoundError):
iterparse("nonexistent")

def test_iterparse_resource_warning(self):
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated
# Test ResourceWarning when iterparse with filename is not closed
import gc
import warnings

# Should emit warning when not closed
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always", ResourceWarning)

def create_unclosed():
context = ET.iterparse(SIMPLE_XMLFILE)
next(context)
# Don't close - should warn

create_unclosed()
gc.collect()

resource_warnings = [x for x in w
if issubclass(x.category, ResourceWarning)]
self.assertGreater(len(resource_warnings), 0,
"Expected ResourceWarning when iterparse is not closed")

# Should NOT warn when explicitly closed
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always", ResourceWarning)

def create_closed():
context = ET.iterparse(SIMPLE_XMLFILE)
next(context)
context.close()

create_closed()
gc.collect()

resource_warnings = [x for x in w
if issubclass(x.category, ResourceWarning)]
self.assertEqual(len(resource_warnings), 0,
"No warning expected when iterparse is properly closed")

# Should NOT warn for file objects (externally managed)
with open(SIMPLE_XMLFILE, 'rb') as source:
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always", ResourceWarning)

def create_with_fileobj():
context = ET.iterparse(source)
next(context)
# Don't close - file object managed externally

create_with_fileobj()
gc.collect()

resource_warnings = [x for x in w
if issubclass(x.category, ResourceWarning)]
self.assertEqual(len(resource_warnings), 0,
"No warning for file objects managed externally")

def test_iterparse_close(self):
iterparse = ET.iterparse

Expand Down
23 changes: 20 additions & 3 deletions Lib/xml/etree/ElementTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,18 +1261,35 @@ def iterator(source):
gen = iterator(source)
class IterParseIterator(collections.abc.Iterator):
__next__ = gen.__next__

def close(self):
if close_source:
source.close()
gen.close()
self._closed = True
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated

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 and not getattr(self, '_closed', False):
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated
try:
warnings.warn(
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated
f"unclosed file {source!r}",
ResourceWarning,
stacklevel=2,
source=self
)
except:
# Ignore errors during warning emission in __del__
# This can happen during interpreter shutdown
pass
Comment thread
serhiy-storchaka marked this conversation as resolved.
Outdated
if close_source:
source.close()
try:
source.close()
Comment on lines +1274 to +1277
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is better?

Suggested change
try:
_warn(f"unclosed iterparse iterator {source.name!r}", ResourceWarning, stacklevel=2)
finally:
source.close()
name = getattr(source, 'name', None)
if name:
_warn("unclosed iterparse iterator %r" % (name,),
ResourceWarning, stacklevel=2)
source.close()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source.name always exist and not empty. This code is only executed when iterparse() opened a file by name.

BTW, if the iterparse() argument was file descriptor 0, your variant would not work.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - I missed that close_source=True guarantees source.name exists, and if name: breaks on fd 0. Thanks for catching that!

The original code is correct. (Though % formatting might be slightly safer than f-string in __del__, but not a blocker.)

except:
# Ignore errors when closing during __del__
pass

it = IterParseIterator()
it._closed = False
it.root = None
wr = weakref.ref(it)
return it
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading