diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 708db4d4387df5e..cb9061042937cf9 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -26,7 +26,7 @@ from test.support import ( findfile, requires_zlib, requires_bz2, requires_lzma, requires_zstd, captured_stdout, captured_stderr, requires_subprocess, - cpython_only, gc_collect + cpython_only, gc_collect, catch_unraisable_exception ) from test.support.os_helper import ( TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath, @@ -4095,6 +4095,21 @@ def test_garbage_collection(self): zf.close() del zf + # ResourceWarning escalated to an error is reported as unraisable, + # and the archive is still closed. + buf = io.BytesIO() + zf = zipfile.ZipFile(buf, "w") + zf.writestr("f.txt", b"data") + with (warnings.catch_warnings(action="error", category=ResourceWarning), + catch_unraisable_exception() as cm): + del zf + gc_collect() + self.assertIs(cm.unraisable.exc_type, ResourceWarning) + + with zipfile.ZipFile(buf, "r") as zf: + self.assertEqual(zf.namelist(), ["f.txt"]) + self.assertEqual(zf.read("f.txt"), b"data") + def test_unsupported_version(self): # File has an extract_version of 120 data = (b'PK\x03\x04x\x00\x00\x00\x00\x00!p\xa1@\x00\x00\x00\x00\x00\x00' diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index d817bdd8769e7d7..c0923cb5c92f232 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -12,7 +12,7 @@ import sys import threading import time -lazy import warnings +import warnings # must stay eager to support finalization try: import zlib # We may need its compression method @@ -2646,14 +2646,18 @@ def mkdir(self, zinfo_or_directory_name, mode=511): self.fp.write(zinfo.FileHeader(False)) self.start_dir = self.fp.tell() - def __del__(self): + def __del__(self, _warn=warnings.warn): """Call the "close()" method in case the user forgot.""" # gh-81954: Warn if writable ZipFile is implicitly closed. # GC cleanup order is non-deterministic and can result in data loss. - if self.fp is not None and self.mode in ('w', 'x', 'a'): - warnings.warn(f"unclosed ZipFile {self!r}", - ResourceWarning, source=self, stacklevel=2) - self.close() + try: + # Wrap within a try to ensure the file is closed even if + # warnings raises. + if self.fp is not None and self.mode in ('w', 'x', 'a'): + _warn(f"unclosed ZipFile {self!r}", + ResourceWarning, source=self, stacklevel=2) + finally: + self.close() def close(self): """Close the file, and for mode 'w', 'x' and 'a' write the ending diff --git a/Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst b/Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst new file mode 100644 index 000000000000000..5ced3b94d223b25 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst @@ -0,0 +1,2 @@ +Fix :class:`zipfile.ZipFile` not being closed at interpreter shutdown when +emitting its :exc:`ResourceWarning` failed with an :exc:`ImportError`.