-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
gh-140601: Add ResourceWarning to iterparse when not closed #140603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
bf3f888
e6c11de
53f63b9
c8ea776
fcd7ca0
0f9b509
6e3b071
86fc36b
329c09c
dd20441
b1f67a4
15c0c5b
a7ef9f2
766d510
53880a6
8a37683
6c5c5e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
@@ -725,6 +728,63 @@ def test_iterparse(self): | |
| with self.assertRaises(FileNotFoundError): | ||
| iterparse("nonexistent") | ||
|
|
||
| def test_iterparse_resource_warning(self): | ||
|
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 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||
|
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): | ||||||||||||||||||||
|
serhiy-storchaka marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| try: | ||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||
|
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 | ||||||||||||||||||||
|
serhiy-storchaka marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| if close_source: | ||||||||||||||||||||
| source.close() | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| source.close() | ||||||||||||||||||||
|
Comment on lines
+1274
to
+1277
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this is better?
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
BTW, if the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right - I missed that The original code is correct. (Though % formatting might be slightly safer than f-string in |
||||||||||||||||||||
| except: | ||||||||||||||||||||
| # Ignore errors when closing during __del__ | ||||||||||||||||||||
| pass | ||||||||||||||||||||
|
|
||||||||||||||||||||
| it = IterParseIterator() | ||||||||||||||||||||
| it._closed = False | ||||||||||||||||||||
| it.root = None | ||||||||||||||||||||
| wr = weakref.ref(it) | ||||||||||||||||||||
| return it | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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. |
Uh oh!
There was an error while loading. Please reload this page.