From bf3f8888b277817bb674b899c564cef27cfd0a12 Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sat, 25 Oct 2025 22:38:06 +0300 Subject: [PATCH 01/12] 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 --- Lib/test/test_xml_etree.py | 90 +++++++++++++++---- Lib/xml/etree/ElementTree.py | 23 ++++- ...-10-25-22-55-07.gh-issue-140601.In3MlS.rst | 4 + 3 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index f65baa0cfae2ad..694332b5b6cb71 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -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) + it = iterparse(SIMPLE_XMLFILE) + del it + import gc + gc.collect() # Ensure previous iterator is cleaned up + # 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) @@ -725,6 +728,63 @@ def test_iterparse(self): with self.assertRaises(FileNotFoundError): iterparse("nonexistent") + def test_iterparse_resource_warning(self): + # 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 diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index dafe5b1b8a0c3f..7641e59b644026 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -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 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): + try: + warnings.warn( + 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 if close_source: - source.close() + try: + source.close() + except: + # Ignore errors when closing during __del__ + pass it = IterParseIterator() + it._closed = False it.root = None wr = weakref.ref(it) return it diff --git a/Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst b/Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst new file mode 100644 index 00000000000000..72666bb8224d63 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst @@ -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. From e6c11de1ba59c128c2711374cd53f8eba3891c12 Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sun, 26 Oct 2025 22:55:36 +0300 Subject: [PATCH 02/12] Address review feedback from serhiy-storchaka - Use nonlocal close_source instead of _closed flag - Bind warnings.warn as default parameter in __del__ - Use assertWarns(ResourceWarning) instead of adding close() calls - Use support.gc_collect() for consistent test behavior - Remove broad exception handling Signed-off-by: Osama Abdelkader --- Lib/test/test_xml_etree.py | 30 ++++++++++++++++-------------- Lib/xml/etree/ElementTree.py | 28 +++++++--------------------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 694332b5b6cb71..211914aab895ee 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -696,14 +696,15 @@ def test_iterparse(self): next(it) self.assertEqual(str(cm.exception), 'junk after document element: line 1, column 12') - it.close() # Close to avoid ResourceWarning - del cm, it + with self.assertWarns(ResourceWarning): + del cm, it + support.gc_collect() # Deleting iterator without close() should emit ResourceWarning (bpo-43292) - it = iterparse(SIMPLE_XMLFILE) - del it - import gc - gc.collect() # Ensure previous iterator is cleaned up + with self.assertWarns(ResourceWarning): + it = iterparse(SIMPLE_XMLFILE) + del it + support.gc_collect() # Explicitly calling close() should not emit warning with warnings_helper.check_no_resource_warning(self): @@ -712,11 +713,12 @@ def test_iterparse(self): del it # 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 self.assertWarns(ResourceWarning): + it = iterparse(SIMPLE_XMLFILE) + action, elem = next(it) + self.assertEqual((action, elem.tag), ('end', 'element')) + del it, elem + support.gc_collect() with warnings_helper.check_no_resource_warning(self): it = iterparse(SIMPLE_XMLFILE) @@ -743,7 +745,7 @@ def create_unclosed(): # Don't close - should warn create_unclosed() - gc.collect() + support.gc_collect() resource_warnings = [x for x in w if issubclass(x.category, ResourceWarning)] @@ -760,7 +762,7 @@ def create_closed(): context.close() create_closed() - gc.collect() + support.gc_collect() resource_warnings = [x for x in w if issubclass(x.category, ResourceWarning)] @@ -778,7 +780,7 @@ def create_with_fileobj(): # Don't close - file object managed externally create_with_fileobj() - gc.collect() + support.gc_collect() resource_warnings = [x for x in w if issubclass(x.category, ResourceWarning)] diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 7641e59b644026..f410d14d55d592 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1261,35 +1261,21 @@ 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() gen.close() - self._closed = True + close_source = False - def __del__(self): - if close_source and not getattr(self, '_closed', False): - try: - warnings.warn( - 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 + def __del__(self, _warn=warnings.warn): if close_source: - try: - source.close() - except: - # Ignore errors when closing during __del__ - pass + _warn(f"unclosed file {source!r}", + ResourceWarning, source=self) + source.close() it = IterParseIterator() - it._closed = False it.root = None wr = weakref.ref(it) return it From 53f63b9971332ce075bfd7037d0fe51dd54eb8c3 Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sun, 26 Oct 2025 22:57:27 +0300 Subject: [PATCH 03/12] remove extra whitespace Signed-off-by: Osama Abdelkader --- Lib/xml/etree/ElementTree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index f410d14d55d592..6fe496308456f0 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1261,7 +1261,7 @@ def iterator(source): gen = iterator(source) class IterParseIterator(collections.abc.Iterator): __next__ = gen.__next__ - + def close(self): nonlocal close_source if close_source: From c8ea7768a3e062132e8bd497559940ab27652c70 Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sun, 26 Oct 2025 23:01:43 +0300 Subject: [PATCH 04/12] remove unused import Signed-off-by: Osama Abdelkader --- Lib/test/test_xml_etree.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 211914aab895ee..be178e974ecf6a 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -698,13 +698,13 @@ def test_iterparse(self): 'junk after document element: line 1, column 12') with self.assertWarns(ResourceWarning): del cm, it - support.gc_collect() + gc_collect() # Deleting iterator without close() should emit ResourceWarning (bpo-43292) with self.assertWarns(ResourceWarning): it = iterparse(SIMPLE_XMLFILE) del it - support.gc_collect() + gc_collect() # Explicitly calling close() should not emit warning with warnings_helper.check_no_resource_warning(self): @@ -718,7 +718,7 @@ def test_iterparse(self): action, elem = next(it) self.assertEqual((action, elem.tag), ('end', 'element')) del it, elem - support.gc_collect() + gc_collect() with warnings_helper.check_no_resource_warning(self): it = iterparse(SIMPLE_XMLFILE) @@ -732,7 +732,6 @@ def test_iterparse(self): def test_iterparse_resource_warning(self): # Test ResourceWarning when iterparse with filename is not closed - import gc import warnings # Should emit warning when not closed @@ -745,7 +744,7 @@ def create_unclosed(): # Don't close - should warn create_unclosed() - support.gc_collect() + gc_collect() resource_warnings = [x for x in w if issubclass(x.category, ResourceWarning)] @@ -762,7 +761,7 @@ def create_closed(): context.close() create_closed() - support.gc_collect() + gc_collect() resource_warnings = [x for x in w if issubclass(x.category, ResourceWarning)] @@ -780,7 +779,7 @@ def create_with_fileobj(): # Don't close - file object managed externally create_with_fileobj() - support.gc_collect() + gc_collect() resource_warnings = [x for x in w if issubclass(x.category, ResourceWarning)] From fcd7ca09a77f22aa6362bce805746c4254d0a5ec Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sun, 26 Oct 2025 23:24:13 +0300 Subject: [PATCH 05/12] Use % formatting instead of f-strings in __del__ F-strings can fail during cleanup. Use % formatting like subprocess.Popen does for safer __del__ behavior. Signed-off-by: Osama Abdelkader --- Lib/xml/etree/ElementTree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 6fe496308456f0..9d48e4933f40c7 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1271,7 +1271,7 @@ def close(self): def __del__(self, _warn=warnings.warn): if close_source: - _warn(f"unclosed file {source!r}", + _warn("unclosed file %r" % (source,), ResourceWarning, source=self) source.close() From 0f9b5094c712f8ca8093017140f1af78a191b965 Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sun, 26 Oct 2025 23:36:28 +0300 Subject: [PATCH 06/12] Add assertions to verify ResourceWarning content Check that warning message contains 'unclosed file' and the filename to ensure it's from iterparse, not the file destructor. Addresses review feedback from serhiy-storchaka. Signed-off-by: Osama Abdelkader --- Lib/test/test_xml_etree.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index be178e974ecf6a..8c7afba2d9849a 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -696,15 +696,19 @@ def test_iterparse(self): next(it) self.assertEqual(str(cm.exception), 'junk after document element: line 1, column 12') - with self.assertWarns(ResourceWarning): + with self.assertWarns(ResourceWarning) as wm: del cm, it gc_collect() + self.assertIn('unclosed file', str(wm.warning)) + self.assertIn(TESTFN, str(wm.warning)) - # Deleting iterator without close() should emit ResourceWarning (bpo-43292) - with self.assertWarns(ResourceWarning): + # Not exhausting the iterator still closes the resource (bpo-43292) + with self.assertWarns(ResourceWarning) as wm: it = iterparse(SIMPLE_XMLFILE) del it gc_collect() + self.assertIn('unclosed file', str(wm.warning)) + self.assertIn(SIMPLE_XMLFILE, str(wm.warning)) # Explicitly calling close() should not emit warning with warnings_helper.check_no_resource_warning(self): @@ -713,12 +717,14 @@ def test_iterparse(self): del it # Not closing before del should emit ResourceWarning - with self.assertWarns(ResourceWarning): + with self.assertWarns(ResourceWarning) as wm: it = iterparse(SIMPLE_XMLFILE) action, elem = next(it) self.assertEqual((action, elem.tag), ('end', 'element')) del it, elem gc_collect() + self.assertIn('unclosed file', str(wm.warning)) + self.assertIn(SIMPLE_XMLFILE, str(wm.warning)) with warnings_helper.check_no_resource_warning(self): it = iterparse(SIMPLE_XMLFILE) From 6e3b0711626669da8e458d09f1bd3fe9bef2876b Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Sun, 26 Oct 2025 23:38:51 +0300 Subject: [PATCH 07/12] Remove redundant test_iterparse_resource_warning function The ResourceWarning tests are already covered in test_iterparse() as noted by serhiy-storchaka in review. Signed-off-by: Osama Abdelkader --- Lib/test/test_xml_etree.py | 56 -------------------------------------- 1 file changed, 56 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 8c7afba2d9849a..870201f757fe00 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -736,62 +736,6 @@ def test_iterparse(self): with self.assertRaises(FileNotFoundError): iterparse("nonexistent") - def test_iterparse_resource_warning(self): - # Test ResourceWarning when iterparse with filename is not closed - 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 From 86fc36b3d3e9d4d5ea670e2b50a99b55332e889d Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Mon, 27 Oct 2025 00:01:49 +0300 Subject: [PATCH 08/12] Remove source=self from ResourceWarning Removing source=self prevents unraisable exception in C extension. The warning message still contains the filename via %r formatting. Signed-off-by: Osama Abdelkader --- Lib/xml/etree/ElementTree.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 9d48e4933f40c7..ee6f5849b7dcda 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1271,8 +1271,7 @@ def close(self): def __del__(self, _warn=warnings.warn): if close_source: - _warn("unclosed file %r" % (source,), - ResourceWarning, source=self) + _warn("unclosed file %r" % (source,), ResourceWarning) source.close() it = IterParseIterator() From 329c09c4054e8257b1573b2b4c2484e1f700a3f5 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 27 Oct 2025 11:29:23 +0200 Subject: [PATCH 09/12] Add explicit close(). --- Lib/test/test_xml_etree.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 870201f757fe00..38096134529e42 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -590,6 +590,7 @@ def test_iterparse(self): ('end', 'root'), ]) self.assertEqual(context.root.tag, 'root') + context.close() context = iterparse(SIMPLE_NS_XMLFILE) self.assertEqual([(action, elem.tag) for action, elem in context], [ @@ -598,6 +599,7 @@ def test_iterparse(self): ('end', '{namespace}empty-element'), ('end', '{namespace}root'), ]) + context.close() with open(SIMPLE_XMLFILE, 'rb') as source: context = iterparse(source) @@ -613,10 +615,12 @@ def test_iterparse(self): events = () context = iterparse(SIMPLE_XMLFILE, events) self.assertEqual([(action, elem.tag) for action, elem in context], []) + context.close() events = () context = iterparse(SIMPLE_XMLFILE, events=events) self.assertEqual([(action, elem.tag) for action, elem in context], []) + context.close() events = ("start", "end") context = iterparse(SIMPLE_XMLFILE, events) @@ -630,6 +634,7 @@ def test_iterparse(self): ('end', 'empty-element'), ('end', 'root'), ]) + context.close() events = ("start", "end", "start-ns", "end-ns") context = iterparse(SIMPLE_NS_XMLFILE, events) @@ -647,6 +652,7 @@ def test_iterparse(self): ('end', '{namespace}root'), ('end-ns', None), ]) + context.close() events = ('start-ns', 'end-ns') context = iterparse(io.StringIO(r""), events) From dd204415282d99ec7bbf51528e2c8a3a7e7bd997 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 27 Oct 2025 12:03:09 +0200 Subject: [PATCH 10/12] Reorganize tests. --- Lib/test/test_xml_etree.py | 32 ++++++++++++++++++++++++-------- Lib/xml/etree/ElementTree.py | 6 ++++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 38096134529e42..0f9754ff2fc2fe 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -702,11 +702,29 @@ def test_iterparse(self): next(it) self.assertEqual(str(cm.exception), 'junk after document element: line 1, column 12') + it.close() + + with self.assertRaises(FileNotFoundError): + iterparse("nonexistent") + + def test_iterparse_not_close(self): + # Not closing before del should emit ResourceWarning + iterparse = ET.iterparse + + it = iterparse(SIMPLE_XMLFILE) + self.assertEqual([(action, elem.tag) for action, elem in it], [ + ('end', 'element'), + ('end', 'element'), + ('end', 'empty-element'), + ('end', 'root'), + ]) + self.assertEqual(it.root.tag, 'root') with self.assertWarns(ResourceWarning) as wm: - del cm, it + del it gc_collect() self.assertIn('unclosed file', str(wm.warning)) - self.assertIn(TESTFN, str(wm.warning)) + self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning)) + self.assertEqual(wm.filename, __file__) # Not exhausting the iterator still closes the resource (bpo-43292) with self.assertWarns(ResourceWarning) as wm: @@ -714,7 +732,8 @@ def test_iterparse(self): del it gc_collect() self.assertIn('unclosed file', str(wm.warning)) - self.assertIn(SIMPLE_XMLFILE, str(wm.warning)) + self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning)) + self.assertEqual(wm.filename, __file__) # Explicitly calling close() should not emit warning with warnings_helper.check_no_resource_warning(self): @@ -722,7 +741,6 @@ def test_iterparse(self): it.close() del it - # Not closing before del should emit ResourceWarning with self.assertWarns(ResourceWarning) as wm: it = iterparse(SIMPLE_XMLFILE) action, elem = next(it) @@ -730,7 +748,8 @@ def test_iterparse(self): del it, elem gc_collect() self.assertIn('unclosed file', str(wm.warning)) - self.assertIn(SIMPLE_XMLFILE, str(wm.warning)) + self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning)) + self.assertEqual(wm.filename, __file__) with warnings_helper.check_no_resource_warning(self): it = iterparse(SIMPLE_XMLFILE) @@ -739,9 +758,6 @@ def test_iterparse(self): self.assertEqual((action, elem.tag), ('end', 'element')) del it, elem - with self.assertRaises(FileNotFoundError): - iterparse("nonexistent") - def test_iterparse_close(self): iterparse = ET.iterparse diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index ee6f5849b7dcda..6a9c2e32abe1b7 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1271,8 +1271,10 @@ def close(self): def __del__(self, _warn=warnings.warn): if close_source: - _warn("unclosed file %r" % (source,), ResourceWarning) - source.close() + try: + _warn(f"unclosed file {source.name!r}", ResourceWarning, stacklevel=2) + finally: + source.close() it = IterParseIterator() it.root = None From b1f67a4eb6506dcdc5eab06e00eee07aca61335c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 27 Oct 2025 12:15:53 +0200 Subject: [PATCH 11/12] Exhausted or failed iterators are auto-closed. --- Lib/test/test_xml_etree.py | 33 +++++++-------------------------- Lib/xml/etree/ElementTree.py | 2 ++ 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 0f9754ff2fc2fe..c4eadbb888cec8 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -590,7 +590,6 @@ def test_iterparse(self): ('end', 'root'), ]) self.assertEqual(context.root.tag, 'root') - context.close() context = iterparse(SIMPLE_NS_XMLFILE) self.assertEqual([(action, elem.tag) for action, elem in context], [ @@ -599,7 +598,6 @@ def test_iterparse(self): ('end', '{namespace}empty-element'), ('end', '{namespace}root'), ]) - context.close() with open(SIMPLE_XMLFILE, 'rb') as source: context = iterparse(source) @@ -615,12 +613,10 @@ def test_iterparse(self): events = () context = iterparse(SIMPLE_XMLFILE, events) self.assertEqual([(action, elem.tag) for action, elem in context], []) - context.close() events = () context = iterparse(SIMPLE_XMLFILE, events=events) self.assertEqual([(action, elem.tag) for action, elem in context], []) - context.close() events = ("start", "end") context = iterparse(SIMPLE_XMLFILE, events) @@ -634,7 +630,6 @@ def test_iterparse(self): ('end', 'empty-element'), ('end', 'root'), ]) - context.close() events = ("start", "end", "start-ns", "end-ns") context = iterparse(SIMPLE_NS_XMLFILE, events) @@ -652,7 +647,6 @@ def test_iterparse(self): ('end', '{namespace}root'), ('end-ns', None), ]) - context.close() events = ('start-ns', 'end-ns') context = iterparse(io.StringIO(r""), events) @@ -698,11 +692,13 @@ def test_iterparse(self): it = iterparse(TESTFN) action, elem = next(it) self.assertEqual((action, elem.tag), ('end', 'document')) - with self.assertRaises(ET.ParseError) as cm: - next(it) - self.assertEqual(str(cm.exception), - 'junk after document element: line 1, column 12') - it.close() + 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 + gc_collect() with self.assertRaises(FileNotFoundError): iterparse("nonexistent") @@ -711,21 +707,6 @@ def test_iterparse_not_close(self): # Not closing before del should emit ResourceWarning iterparse = ET.iterparse - it = iterparse(SIMPLE_XMLFILE) - self.assertEqual([(action, elem.tag) for action, elem in it], [ - ('end', 'element'), - ('end', 'element'), - ('end', 'empty-element'), - ('end', 'root'), - ]) - self.assertEqual(it.root.tag, 'root') - with self.assertWarns(ResourceWarning) as wm: - del it - gc_collect() - self.assertIn('unclosed file', str(wm.warning)) - self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning)) - self.assertEqual(wm.filename, __file__) - # Not exhausting the iterator still closes the resource (bpo-43292) with self.assertWarns(ResourceWarning) as wm: it = iterparse(SIMPLE_XMLFILE) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 6a9c2e32abe1b7..0bc57e6ba11cd6 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -1255,8 +1255,10 @@ def iterator(source): if it is not None: it.root = root finally: + nonlocal close_source if close_source: source.close() + close_source = False gen = iterator(source) class IterParseIterator(collections.abc.Iterator): From 15c0c5b99dd5bd0308b7da3cb35b9da8ff9b04f2 Mon Sep 17 00:00:00 2001 From: Osama Abdelkader Date: Mon, 27 Oct 2025 14:30:52 +0300 Subject: [PATCH 12/12] Add versionchanged directive for ResourceWarning Document that iterparse now emits ResourceWarning in Python 3.15 when not explicitly closed. Signed-off-by: Osama Abdelkader --- Doc/library/xml.etree.elementtree.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 881708a4dd702e..e59759683a6d4c 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -656,6 +656,10 @@ Functions .. versionchanged:: 3.13 Added the :meth:`!close` method. + .. versionchanged:: 3.15 + A :exc:`ResourceWarning` is now emitted if the iterator opened a file + and is not explicitly closed. + .. function:: parse(source, parser=None)