Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ ZipFile objects
Added support for specifying member name encoding for reading
metadata in the zipfile's directory and file headers.

.. versionchanged:: next
Deleting a writable, open :class:`zipfile.ZipFile` now emits a
:exc:`ResourceWarning`. Use as a :term:`context manager` or call
:meth:`~zipfile.ZipFile.close` explicitly.

.. method:: ZipFile.close()

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_bigmem.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ def test_repeat_small(self, size):
def test_repeat_large(self, size):
return self.basic_test_repeat(size)

@bigmemtest(size=_1G - 1, memuse=12)
@bigmemtest(size=_1G - 1, memuse=pointer_size * 3)
def test_repeat_large_2(self, size):
return self.basic_test_repeat(size)

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_hashlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,12 +691,12 @@ def test_case_md5_2(self):
)

@unittest.skipIf(sys.maxsize < _4G + 5, 'test cannot run on 32-bit systems')
@bigmemtest(size=_4G + 5, memuse=1, dry_run=False)
@bigmemtest(size=_4G + 5, memuse=2, dry_run=False)
def test_case_md5_huge(self, size):
self.check('md5', b'A'*size, 'c9af2dff37468ce5dfee8f2cfc0a9c6d')

@unittest.skipIf(sys.maxsize < _4G - 1, 'test cannot run on 32-bit systems')
@bigmemtest(size=_4G - 1, memuse=1, dry_run=False)
@bigmemtest(size=_4G - 1, memuse=2, dry_run=False)
def test_case_md5_uintmax(self, size):
self.check('md5', b'A'*size, '28138d306ff1b8281f1a9067e1a1a2b3')

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_re.py
Original file line number Diff line number Diff line change
Expand Up @@ -2251,7 +2251,7 @@ def test_large_search(self, size):

# The huge memuse is because of re.sub() using a list and a join()
# to create the replacement result.
@bigmemtest(size=_2G, memuse=16 + 2)
@bigmemtest(size=_2G, memuse=16 + 3)
def test_large_subn(self, size):
# Issue #10182: indices were 32-bit-truncated.
s = 'a' * size
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_tcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ def test_huge_string_call(self, size):

@support.cpython_only
@unittest.skipUnless(INT_MAX < PY_SSIZE_T_MAX, "needs UINT_MAX < SIZE_MAX")
@support.bigmemtest(size=INT_MAX + 1, memuse=2, dry_run=False)
@support.bigmemtest(size=INT_MAX + 1, memuse=3, dry_run=False)
def test_huge_string_builtins(self, size):
tk = self.interp.tk
value = '1' + ' ' * size
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_zipfile/_path/_test_params.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import types
from contextlib import AbstractContextManager

from ._itertools import always_iterable

Expand All @@ -9,6 +10,8 @@ def parameterize(names, value_groups):
Decorate a test method to run it as a set of subtests.

Modeled after pytest.parametrize.

Context Manager types are entered and exited.
"""

def decorator(func):
Expand All @@ -17,6 +20,9 @@ def wrapped(self):
for values in value_groups:
resolved = map(Invoked.eval, always_iterable(values))
params = dict(zip(always_iterable(names), resolved))
for value in params.values():
if isinstance(value, AbstractContextManager):
self.enterContext(value)
with self.subTest(**params):
func(self, **params)

Expand Down
25 changes: 11 additions & 14 deletions Lib/test/test_zipfile/_path/test_path.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import contextlib
import io
import itertools
import pathlib
Expand Down Expand Up @@ -83,12 +82,8 @@ def build_alpharep_fixture():


class TestPath(unittest.TestCase):
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)

def zipfile_ondisk(self, alpharep):
tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
tmpdir = pathlib.Path(self.enterContext(temp_dir()))
buffer = alpharep.fp
alpharep.close()
path = tmpdir / alpharep.filename
Expand Down Expand Up @@ -145,7 +140,7 @@ def test_open(self, alpharep):

def test_open_encoding_utf16(self):
in_memory_file = io.BytesIO()
zf = zipfile.ZipFile(in_memory_file, "w")
zf = self.enterContext(zipfile.ZipFile(in_memory_file, "w"))
zf.writestr("path/16.txt", "This was utf-16".encode("utf-16"))
zf.filename = "test_open_utf16.zip"
root = zipfile.Path(zf)
Expand All @@ -160,7 +155,7 @@ def test_open_encoding_utf16(self):

def test_open_encoding_errors(self):
in_memory_file = io.BytesIO()
zf = zipfile.ZipFile(in_memory_file, "w")
zf = self.enterContext(zipfile.ZipFile(in_memory_file, "w"))
zf.writestr("path/bad-utf8.bin", b"invalid utf-8: \xff\xff.")
zf.filename = "test_read_text_encoding_errors.zip"
root = zipfile.Path(zf)
Expand Down Expand Up @@ -204,7 +199,8 @@ def test_open_write(self):
If the zipfile is open for write, it should be possible to
write bytes or text to it.
"""
zf = zipfile.Path(zipfile.ZipFile(io.BytesIO(), mode='w'))
zip_file = self.enterContext(zipfile.ZipFile(io.BytesIO(), mode='w'))
zf = zipfile.Path(zip_file)
with zf.joinpath('file.bin').open('wb') as strm:
strm.write(b'binary contents')
with zf.joinpath('file.txt').open('w', encoding="utf-8") as strm:
Expand Down Expand Up @@ -319,7 +315,7 @@ def test_mutability(self, alpharep):
def huge_zipfile(self):
"""Create a read-only zipfile with a huge number of entries."""
strm = io.BytesIO()
zf = zipfile.ZipFile(strm, "w")
zf = self.enterContext(zipfile.ZipFile(strm, "w"))
for entry in map(str, range(self.HUGE_ZIPFILE_NUM_ENTRIES)):
zf.writestr(entry, entry)
zf.mode = 'r'
Expand Down Expand Up @@ -530,7 +526,8 @@ def test_glob_chars(self, alpharep):
]

def test_glob_empty(self):
root = zipfile.Path(zipfile.ZipFile(io.BytesIO(), 'w'))
zip_file = self.enterContext(zipfile.ZipFile(io.BytesIO(), 'w'))
root = zipfile.Path(zip_file)
with self.assertRaises(ValueError):
root.glob('')

Expand Down Expand Up @@ -614,7 +611,7 @@ def test_malformed_paths(self):
Paths with dots are treated like regular files.
"""
data = io.BytesIO()
zf = zipfile.ZipFile(data, "w")
zf = self.enterContext(zipfile.ZipFile(data, "w"))
zf.writestr("/one-slash.txt", b"content")
zf.writestr("//two-slash.txt", b"content")
zf.writestr("../parent.txt", b"content")
Expand All @@ -632,7 +629,7 @@ def test_unsupported_names(self):
in the zip file.
"""
data = io.BytesIO()
zf = zipfile.ZipFile(data, "w")
zf = self.enterContext(zipfile.ZipFile(data, "w"))
zf.writestr("path?", b"content")
zf.writestr("V: NMS.flac", b"fLaC...")
zf.filename = ''
Expand All @@ -647,7 +644,7 @@ def test_backslash_not_separator(self):
In a zip file, backslashes are not separators.
"""
data = io.BytesIO()
zf = zipfile.ZipFile(data, "w")
zf = self.enterContext(zipfile.ZipFile(data, "w"))
zf.writestr(DirtyZipInfo("foo\\bar")._for_archive(zf), b"content")
zf.filename = ''
root = zipfile.Path(zf)
Expand Down
33 changes: 28 additions & 5 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@
from test.support import (
findfile, requires_zlib, requires_bz2, requires_lzma,
requires_zstd, captured_stdout, captured_stderr, requires_subprocess,
cpython_only
cpython_only, gc_collect
)
from test.support.os_helper import (
TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath
)
from test.support.import_helper import ensure_lazy_imports
from test.support.warnings_helper import check_no_resource_warning


TESTFN2 = TESTFN + "2"
Expand Down Expand Up @@ -4058,6 +4059,28 @@ def test_close_on_exception(self):
except zipfile.BadZipFile:
self.assertIsNone(zipfp2.fp, 'zipfp is not closed')

def test_garbage_collection(self):
# gh-81954: Warn if a writable zipfile is closed by GC.
with self.assertWarns(ResourceWarning):
zipfile.ZipFile(io.BytesIO(), "w")
gc_collect()

# Only warn if there is possible data loss.
# Properly closed via context manager.
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("f.txt", b"data")

with check_no_resource_warning(self):
# Read mode: No possible data loss.
zipfile.ZipFile(buf, "r")

# Write with manual explicit close: No pending data.
zf = zipfile.ZipFile(io.BytesIO(), "w")
zf.writestr("f.txt", b"data")
zf.close()
del zf

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'
Expand Down Expand Up @@ -5510,10 +5533,10 @@ def test_root_folder_in_zipfile(self):
the zip file, this is a strange behavior, but we should support it.
"""
in_memory_file = io.BytesIO()
zf = zipfile.ZipFile(in_memory_file, "w")
zf.mkdir('/')
zf.writestr('./a.txt', 'aaa')
zf.extractall(TESTFN2)
with zipfile.ZipFile(in_memory_file, "w") as zf:
zf.mkdir('/')
zf.writestr('./a.txt', 'aaa')
zf.extractall(TESTFN2)

def tearDown(self):
rmtree(TESTFN2)
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_zlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ def testEOFError(self):
self.assertRaises(EOFError, zlibd.decompress, b"")

@support.skip_if_pgo_task
@bigmemtest(size=_4G + 100, memuse=3.3)
@bigmemtest(size=_4G + 100, memuse=4.5)
def testDecompress4G(self, size):
# "Test zlib._ZlibDecompressor.decompress() with >4GiB input"
blocksize = min(10 * 1024 * 1024, size)
Expand Down
6 changes: 6 additions & 0 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
import threading
import time
lazy import warnings

try:
import zlib # We may need its compression method
Expand Down Expand Up @@ -2616,6 +2617,11 @@ def mkdir(self, zinfo_or_directory_name, mode=511):

def __del__(self):
"""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()

def close(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Deleting a writable, open :class:`zipfile.ZipFile` now emits a
:exc:`ResourceWarning`. Use as a :term:`context manager`
or call :meth:`~zipfile.ZipFile.close` explicitly.
Loading