Skip to content

Commit 54742a1

Browse files
committed
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 <[email protected]>
1 parent b3c713a commit 54742a1

File tree

3 files changed

+90
-17
lines changed

3 files changed

+90
-17
lines changed

Lib/test/test_xml_etree.py

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -692,28 +692,31 @@ def test_iterparse(self):
692692
it = iterparse(TESTFN)
693693
action, elem = next(it)
694694
self.assertEqual((action, elem.tag), ('end', 'document'))
695-
with warnings_helper.check_no_resource_warning(self):
696-
with self.assertRaises(ET.ParseError) as cm:
697-
next(it)
698-
self.assertEqual(str(cm.exception),
699-
'junk after document element: line 1, column 12')
700-
del cm, it
695+
with self.assertRaises(ET.ParseError) as cm:
696+
next(it)
697+
self.assertEqual(str(cm.exception),
698+
'junk after document element: line 1, column 12')
699+
it.close() # Close to avoid ResourceWarning
700+
del cm, it
701701

702-
# Not exhausting the iterator still closes the resource (bpo-43292)
703-
with warnings_helper.check_no_resource_warning(self):
704-
it = iterparse(SIMPLE_XMLFILE)
705-
del it
702+
# Deleting iterator without close() should emit ResourceWarning (bpo-43292)
703+
it = iterparse(SIMPLE_XMLFILE)
704+
del it
705+
import gc
706+
gc.collect() # Ensure previous iterator is cleaned up
706707

708+
# Explicitly calling close() should not emit warning
707709
with warnings_helper.check_no_resource_warning(self):
708710
it = iterparse(SIMPLE_XMLFILE)
709711
it.close()
710712
del it
711713

712-
with warnings_helper.check_no_resource_warning(self):
713-
it = iterparse(SIMPLE_XMLFILE)
714-
action, elem = next(it)
715-
self.assertEqual((action, elem.tag), ('end', 'element'))
716-
del it, elem
714+
# Not closing before del should emit ResourceWarning
715+
it = iterparse(SIMPLE_XMLFILE)
716+
action, elem = next(it)
717+
self.assertEqual((action, elem.tag), ('end', 'element'))
718+
del it, elem
719+
gc.collect() # Ensure previous iterator is cleaned up
717720

718721
with warnings_helper.check_no_resource_warning(self):
719722
it = iterparse(SIMPLE_XMLFILE)
@@ -725,6 +728,63 @@ def test_iterparse(self):
725728
with self.assertRaises(FileNotFoundError):
726729
iterparse("nonexistent")
727730

731+
def test_iterparse_resource_warning(self):
732+
# Test ResourceWarning when iterparse with filename is not closed
733+
import gc
734+
import warnings
735+
736+
# Should emit warning when not closed
737+
with warnings.catch_warnings(record=True) as w:
738+
warnings.simplefilter("always", ResourceWarning)
739+
740+
def create_unclosed():
741+
context = ET.iterparse(SIMPLE_XMLFILE)
742+
next(context)
743+
# Don't close - should warn
744+
745+
create_unclosed()
746+
gc.collect()
747+
748+
resource_warnings = [x for x in w
749+
if issubclass(x.category, ResourceWarning)]
750+
self.assertGreater(len(resource_warnings), 0,
751+
"Expected ResourceWarning when iterparse is not closed")
752+
753+
# Should NOT warn when explicitly closed
754+
with warnings.catch_warnings(record=True) as w:
755+
warnings.simplefilter("always", ResourceWarning)
756+
757+
def create_closed():
758+
context = ET.iterparse(SIMPLE_XMLFILE)
759+
next(context)
760+
context.close()
761+
762+
create_closed()
763+
gc.collect()
764+
765+
resource_warnings = [x for x in w
766+
if issubclass(x.category, ResourceWarning)]
767+
self.assertEqual(len(resource_warnings), 0,
768+
"No warning expected when iterparse is properly closed")
769+
770+
# Should NOT warn for file objects (externally managed)
771+
with open(SIMPLE_XMLFILE, 'rb') as source:
772+
with warnings.catch_warnings(record=True) as w:
773+
warnings.simplefilter("always", ResourceWarning)
774+
775+
def create_with_fileobj():
776+
context = ET.iterparse(source)
777+
next(context)
778+
# Don't close - file object managed externally
779+
780+
create_with_fileobj()
781+
gc.collect()
782+
783+
resource_warnings = [x for x in w
784+
if issubclass(x.category, ResourceWarning)]
785+
self.assertEqual(len(resource_warnings), 0,
786+
"No warning for file objects managed externally")
787+
728788
def test_iterparse_close(self):
729789
iterparse = ET.iterparse
730790

Lib/xml/etree/ElementTree.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,18 +1261,27 @@ def iterator(source):
12611261
gen = iterator(source)
12621262
class IterParseIterator(collections.abc.Iterator):
12631263
__next__ = gen.__next__
1264+
12641265
def close(self):
12651266
if close_source:
12661267
source.close()
12671268
gen.close()
1269+
self._closed = True
12681270

12691271
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+
if close_source and not getattr(self, '_closed', False):
1273+
import warnings
1274+
warnings.warn(
1275+
f"unclosed file {source!r}",
1276+
ResourceWarning,
1277+
stacklevel=2,
1278+
source=self
1279+
)
12721280
if close_source:
12731281
source.close()
12741282

12751283
it = IterParseIterator()
1284+
it._closed = False
12761285
it.root = None
12771286
wr = weakref.ref(it)
12781287
return it
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)