Skip to content

Commit f9f33a6

Browse files
authored
gh-81954: Eagerly import warnings module to failure of ZipFile.__del__ during finalization (#159419)
ZipFile.__del__ emitted its ResourceWarning before calling close(). When warnings.warn() raised, close() was skipped and the central directory of a writable archive was never written, leaving a corrupt file. This happens at interpreter shutdown, where resolving the lazy "warnings" import raises ImportError, and with -W error::ResourceWarning at any time.
1 parent 1c75034 commit f9f33a6

3 files changed

Lines changed: 28 additions & 7 deletions

File tree

‎Lib/test/test_zipfile/test_core.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from test.support import (
2727
findfile, requires_zlib, requires_bz2, requires_lzma,
2828
requires_zstd, captured_stdout, captured_stderr, requires_subprocess,
29-
cpython_only, gc_collect
29+
cpython_only, gc_collect, catch_unraisable_exception
3030
)
3131
from test.support.os_helper import (
3232
TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath,
@@ -4095,6 +4095,21 @@ def test_garbage_collection(self):
40954095
zf.close()
40964096
del zf
40974097

4098+
# ResourceWarning escalated to an error is reported as unraisable,
4099+
# and the archive is still closed.
4100+
buf = io.BytesIO()
4101+
zf = zipfile.ZipFile(buf, "w")
4102+
zf.writestr("f.txt", b"data")
4103+
with (warnings.catch_warnings(action="error", category=ResourceWarning),
4104+
catch_unraisable_exception() as cm):
4105+
del zf
4106+
gc_collect()
4107+
self.assertIs(cm.unraisable.exc_type, ResourceWarning)
4108+
4109+
with zipfile.ZipFile(buf, "r") as zf:
4110+
self.assertEqual(zf.namelist(), ["f.txt"])
4111+
self.assertEqual(zf.read("f.txt"), b"data")
4112+
40984113
def test_unsupported_version(self):
40994114
# File has an extract_version of 120
41004115
data = (b'PK\x03\x04x\x00\x00\x00\x00\x00!p\xa1@\x00\x00\x00\x00\x00\x00'

‎Lib/zipfile/__init__.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import sys
1313
import threading
1414
import time
15-
lazy import warnings
15+
import warnings # must stay eager to support finalization
1616

1717
try:
1818
import zlib # We may need its compression method
@@ -2646,14 +2646,18 @@ def mkdir(self, zinfo_or_directory_name, mode=511):
26462646
self.fp.write(zinfo.FileHeader(False))
26472647
self.start_dir = self.fp.tell()
26482648

2649-
def __del__(self):
2649+
def __del__(self, _warn=warnings.warn):
26502650
"""Call the "close()" method in case the user forgot."""
26512651
# gh-81954: Warn if writable ZipFile is implicitly closed.
26522652
# GC cleanup order is non-deterministic and can result in data loss.
2653-
if self.fp is not None and self.mode in ('w', 'x', 'a'):
2654-
warnings.warn(f"unclosed ZipFile {self!r}",
2655-
ResourceWarning, source=self, stacklevel=2)
2656-
self.close()
2653+
try:
2654+
# Wrap within a try to ensure the file is closed even if
2655+
# warnings raises.
2656+
if self.fp is not None and self.mode in ('w', 'x', 'a'):
2657+
_warn(f"unclosed ZipFile {self!r}",
2658+
ResourceWarning, source=self, stacklevel=2)
2659+
finally:
2660+
self.close()
26572661

26582662
def close(self):
26592663
"""Close the file, and for mode 'w', 'x' and 'a' write the ending
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :class:`zipfile.ZipFile` not being closed at interpreter shutdown when
2+
emitting its :exc:`ResourceWarning` failed with an :exc:`ImportError`.

0 commit comments

Comments
 (0)