Skip to content

Commit a9e15e5

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 a9e15e5

File tree

3 files changed

+89
-17
lines changed

3 files changed

+89
-17
lines changed

Lib/test/test_xml_etree.py

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -692,28 +692,28 @@ 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
706705

706+
# Explicitly calling close() should not emit warning
707707
with warnings_helper.check_no_resource_warning(self):
708708
it = iterparse(SIMPLE_XMLFILE)
709709
it.close()
710710
del it
711711

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
712+
# Not closing before del should emit ResourceWarning
713+
it = iterparse(SIMPLE_XMLFILE)
714+
action, elem = next(it)
715+
self.assertEqual((action, elem.tag), ('end', 'element'))
716+
del it, elem
717717

718718
with warnings_helper.check_no_resource_warning(self):
719719
it = iterparse(SIMPLE_XMLFILE)
@@ -725,6 +725,63 @@ def test_iterparse(self):
725725
with self.assertRaises(FileNotFoundError):
726726
iterparse("nonexistent")
727727

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

Lib/xml/etree/ElementTree.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,14 +1261,25 @@ def iterator(source):
12611261
gen = iterator(source)
12621262
class IterParseIterator(collections.abc.Iterator):
12631263
__next__ = gen.__next__
1264+
1265+
def __init__(self):
1266+
self._closed = False
1267+
12641268
def close(self):
12651269
if close_source:
12661270
source.close()
12671271
gen.close()
1272+
self._closed = True
12681273

12691274
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.)
1275+
if close_source and not self._closed:
1276+
import warnings
1277+
warnings.warn(
1278+
f"unclosed file {source!r}",
1279+
ResourceWarning,
1280+
stacklevel=2,
1281+
source=self
1282+
)
12721283
if close_source:
12731284
source.close()
12741285

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)