diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 7ade7fbc326617..da7cb555dd355b 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -177,7 +177,7 @@ ZipFile objects .. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True, \ - compresslevel=None, *, strict_timestamps=True, \ + compresslevel=None, preset=None, *, strict_timestamps=True, \ metadata_encoding=None) Open a ZIP file, where *file* can be a path to a file (a string), a @@ -221,6 +221,12 @@ ZipFile objects :attr:`CompressionParameter.compression_level ` for more on retrieving valid values and their meaning). + The *preset* parameter controls the LZMA preset to use when + writing files to the archive. + When using :const:`ZIP_LZMA` integers ``0`` through ``9`` are accepted + (see :class:`lzma ` for more information). + When using :const:`ZIP_STORED`, :const:`ZIP_DEFLATED`, :const:`ZIP_BZIP2` it has no effect. + The *strict_timestamps* argument, when set to ``False``, allows to zip files older than 1980-01-01 at the cost of setting the timestamp to 1980-01-01. @@ -285,6 +291,9 @@ ZipFile objects Added support for specifying member name encoding for reading metadata in the zipfile's directory and file headers. + .. versionadded:: 3.12 + Added the *preset* parameter. + .. versionchanged:: 3.16 Deleting a writable, open :class:`zipfile.ZipFile` now emits a :exc:`ResourceWarning`. Use as a :term:`context manager` or call @@ -466,13 +475,13 @@ ZipFile objects .. method:: ZipFile.write(filename, arcname=None, compress_type=None, \ - compresslevel=None) + compresslevel=None, preset=None) Write the file named *filename* to the archive, giving it the archive name *arcname* (by default, this will be the same as *filename*, but without a drive letter and with leading path separators removed). If given, *compress_type* overrides the value given for the *compression* parameter to the constructor for - the new entry. Similarly, *compresslevel* will override the constructor if + the new entry. Similarly, *compresslevel* and *preset* will override the constructor if given. The archive must be open with mode ``'w'``, ``'x'`` or ``'a'``. @@ -507,7 +516,7 @@ ZipFile objects .. method:: ZipFile.writestr(zinfo_or_arcname, data, compress_type=None, \ - compresslevel=None) + compresslevel=None, preset=None) Write a file into the archive. The contents is *data*, which may be either a :class:`str` or a :class:`bytes` instance; if it is a :class:`str`, @@ -519,8 +528,8 @@ ZipFile objects If given, *compress_type* overrides the value given for the *compression* parameter to the constructor for the new entry, or in the *zinfo_or_arcname* - (if that is a :class:`ZipInfo` instance). Similarly, *compresslevel* will - override the constructor if given. + (if that is a :class:`ZipInfo` instance). Similarly, *compresslevel* and + *preset* will override the constructor if given. .. note:: diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 708db4d4387df5..e76b64d4b8f3cc 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -77,8 +77,8 @@ def setUp(self): with open(TESTFN, "wb") as fp: fp.write(self.data) - def make_test_archive(self, f, compression, compresslevel=None): - kwargs = {'compression': compression, 'compresslevel': compresslevel} + def make_test_archive(self, f, compression, compresslevel=None, preset=None): + kwargs = {'compression': compression, 'compresslevel': compresslevel, 'preset': preset} # Create the ZIP archive with zipfile.ZipFile(f, "w", **kwargs) as zipfp: zipfp.write(TESTFN, "another.name") @@ -88,8 +88,8 @@ def make_test_archive(self, f, compression, compresslevel=None): for line in self.line_gen: f.write(line) - def zip_test(self, f, compression, compresslevel=None): - self.make_test_archive(f, compression, compresslevel) + def zip_test(self, f, compression, compresslevel=None, preset=None): + self.make_test_archive(f, compression, compresslevel, preset=None) # Read the ZIP archive with zipfile.ZipFile(f, "r", compression) as zipfp: @@ -339,6 +339,22 @@ def test_writestr_compresslevel(self): self.assertEqual(b_info.compress_type, self.compression) self.assertEqual(b_info._compresslevel, 2) + def test_writestr_preset(self): + with zipfile.ZipFile(TESTFN2, "w", preset=1) as zipfp: + zipfp.writestr("a.txt", "hello world", compress_type=self.compression) + zipfp.writestr("b.txt", "hello world", compress_type=self.compression, + preset=2) + + # Preset follows the constructor. + a_info = zipfp.getinfo('a.txt') + self.assertEqual(a_info.compress_type, self.compression) + self.assertEqual(a_info._preset, 1) + + # Preset is overridden. + b_info = zipfp.getinfo('b.txt') + self.assertEqual(b_info.compress_type, self.compression) + self.assertEqual(b_info._preset, 2) + def test_read_return_size(self): # Issue #9837: ZipExtFile.read() shouldn't return more bytes # than requested. @@ -415,6 +431,10 @@ def test_compresslevel_basic(self): for f in get_files(self): self.zip_test(f, self.compression, compresslevel=9) + def test_preset_basic(self): + for f in get_files(self): + self.zip_test(f, self.compression, preset=9) + def test_per_file_compresslevel(self): """Check that files within a Zip archive can have different compression levels.""" @@ -426,6 +446,17 @@ def test_per_file_compresslevel(self): self.assertEqual(one_info._compresslevel, 1) self.assertEqual(nine_info.compress_level, 9) + def test_per_file_preset(self): + """Check that files within a Zip archive can have different + presets.""" + with zipfile.ZipFile(TESTFN2, "w", preset=1) as zipfp: + zipfp.write(TESTFN, 'compress_1') + zipfp.write(TESTFN, 'compress_9', preset=9) + one_info = zipfp.getinfo('compress_1') + nine_info = zipfp.getinfo('compress_9') + self.assertEqual(one_info._preset, 1) + self.assertEqual(nine_info._preset, 9) + def test_writing_errors(self): class BrokenFile(io.BytesIO): def write(self, data): diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 87337478912e57..6909b3296bc76e 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -438,6 +438,7 @@ class ZipInfo: 'date_time', 'compress_type', 'compress_level', + '_preset', 'comment', 'extra', 'create_system', @@ -472,6 +473,7 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): # Standard values: self.compress_type = ZIP_STORED # Type of compression for the file self.compress_level = None # Level for the compressor + self._preset = None # Preset for LZMA compression self.comment = b"" # Comment for each file self.extra = b"" # ZIP extra data if sys.platform == 'win32': @@ -685,6 +687,7 @@ def _for_archive(self, archive): self.compress_type = archive.compression self.compress_level = archive.compresslevel + self._preset = archive.preset if self.filename.endswith('/'): # pragma: no cover self.external_attr = 0o40775 << 16 # drwxrwxr-x self.external_attr |= 0x10 # MS-DOS directory flag @@ -766,14 +769,22 @@ def decrypter(data): class LZMACompressor: - def __init__(self): + def __init__(self, preset=None): self._comp = None + self.preset = preset def _init(self): - props = lzma._encode_filter_properties({'id': lzma.FILTER_LZMA1}) - self._comp = lzma.LZMACompressor(lzma.FORMAT_RAW, filters=[ - lzma._decode_filter_properties(lzma.FILTER_LZMA1, props) - ]) + filter_properties = {'id': lzma.FILTER_LZMA1} + if self.preset: + filter_properties["preset"] = self.preset + props = lzma._encode_filter_properties(filter_properties) + + self._comp = lzma.LZMACompressor( + lzma.FORMAT_RAW, + filters=[ + lzma._decode_filter_properties(lzma.FILTER_LZMA1, props) + ] + ) return struct.pack('