From 54b7e31d1c26667adbe884afc94bf93a7b0869a3 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Fri, 25 Jul 2025 14:36:02 +0100 Subject: [PATCH 01/10] Add private members via init to make it easier to subclass. --- Lib/zipfile/__init__.py | 39 ++++++++++++------- ...5-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst | 2 + 2 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 2969f735e8abb9d..d9702e107cbdb70 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1372,7 +1372,7 @@ class ZipFile: """ Class with methods to open, read, write, close, list zip files. z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True, - compresslevel=None) + compresslevel=None, _ZipInfo=ZipInfo, _ZipExtFile=ZipExtFile) file: Either the path to the file, or a file-like object. If it is a path, the file will be opened and closed by ZipFile. @@ -1392,21 +1392,32 @@ class ZipFile: When using ZIP_ZSTANDARD integers -7 though 22 are common, see the CompressionParameter enum in compression.zstd for details. - + _ZipInfo: A class that can replace ZipInfo. This is designed to help extend + ZipFile, for example to implement other encryption or compression + methods. + This is private as there is no commitemnt to maintain backward + compatibitly. + _ZipExtFile: A class that can replace ZipExtFile. This is designed to help + extend ZipFile, for example to implement other encryption + or compression methods. + This is private as there is no commitemnt to maintain backward + compatibitly. """ fp = None # Set here since __del__ checks it _windows_illegal_name_trans_table = None def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True, - compresslevel=None, *, strict_timestamps=True, metadata_encoding=None): + compresslevel=None, *, strict_timestamps=True, metadata_encoding=None, + _ZipInfo=ZipInfo, _ZipExtFile=ZipExtFile): """Open the ZIP file with mode read 'r', write 'w', exclusive create 'x', or append 'a'.""" if mode not in ('r', 'w', 'x', 'a'): raise ValueError("ZipFile requires mode 'r', 'w', 'x', or 'a'") _check_compression(compression) - + self._ZipInfo = _ZipInfo + self._ZipExtFile = _ZipExtFile self._allowZip64 = allowZip64 self._didModify = False self.debug = 0 # Level of printing: 0 through 3 @@ -1558,7 +1569,7 @@ def _RealGetContents(self): # Historical ZIP filename encoding filename = filename.decode(self.metadata_encoding or 'cp437') # Create ZipInfo instance to store file information - x = ZipInfo(filename) + x = self._ZipInfo(filename) x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH]) x.comment = fp.read(centdir[_CD_COMMENT_LENGTH]) x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET] @@ -1693,11 +1704,11 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False): "Attempt to use ZIP archive that was already closed") # Make sure we have an info object - if isinstance(name, ZipInfo): + if isinstance(name, self._ZipInfo): # 'name' is already an info object zinfo = name elif mode == 'w': - zinfo = ZipInfo(name) + zinfo = self._ZipInfo(name) zinfo.compress_type = self.compression zinfo.compress_level = self.compresslevel else: @@ -1774,7 +1785,7 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False): else: pwd = None - return ZipExtFile(zef_file, mode + 'b', zinfo, pwd, True) + return self._ZipExtFile(zef_file, mode + 'b', zinfo, pwd, True) except: zef_file.close() raise @@ -1872,7 +1883,7 @@ def _extract_member(self, member, targetpath, pwd): """Extract the ZipInfo object 'member' to a physical file on the path targetpath. """ - if not isinstance(member, ZipInfo): + if not isinstance(member, self._ZipInfo): member = self.getinfo(member) # build the destination pathname, replacing @@ -1952,7 +1963,7 @@ def write(self, filename, arcname=None, "Can't write to ZIP archive while an open writing handle exists" ) - zinfo = ZipInfo.from_file(filename, arcname, + zinfo = self._ZipInfo.from_file(filename, arcname, strict_timestamps=self._strict_timestamps) if zinfo.is_dir(): @@ -1982,10 +1993,10 @@ def writestr(self, zinfo_or_arcname, data, the name of the file in the archive.""" if isinstance(data, str): data = data.encode("utf-8") - if isinstance(zinfo_or_arcname, ZipInfo): + if isinstance(zinfo_or_arcname, self._ZipInfo): zinfo = zinfo_or_arcname else: - zinfo = ZipInfo(zinfo_or_arcname)._for_archive(self) + zinfo = self._ZipInfo(zinfo_or_arcname)._for_archive(self) if not self.fp: raise ValueError( @@ -2008,7 +2019,7 @@ def writestr(self, zinfo_or_arcname, data, def mkdir(self, zinfo_or_directory_name, mode=511): """Creates a directory inside the zip archive.""" - if isinstance(zinfo_or_directory_name, ZipInfo): + if isinstance(zinfo_or_directory_name, self._ZipInfo): zinfo = zinfo_or_directory_name if not zinfo.is_dir(): raise ValueError("The given ZipInfo does not describe a directory") @@ -2016,7 +2027,7 @@ def mkdir(self, zinfo_or_directory_name, mode=511): directory_name = zinfo_or_directory_name if not directory_name.endswith("/"): directory_name += "/" - zinfo = ZipInfo(directory_name) + zinfo = self._ZipInfo(directory_name) zinfo.compress_size = 0 zinfo.CRC = 0 zinfo.external_attr = ((0o40000 | mode) & 0xFFFF) << 16 diff --git a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst new file mode 100644 index 000000000000000..678c2273c9151c0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst @@ -0,0 +1,2 @@ +zipfile.ZipFile.__init__ was updated to make it easier to subclass and +extend it to support new encryption methods. From 609d44f489c6fc80f89a05f364e4015461395f7f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Fri, 25 Jul 2025 16:22:33 +0100 Subject: [PATCH 02/10] Apply suggestions from code review --- Lib/zipfile/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index d9702e107cbdb70..52f87ac2d9d4624 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1372,7 +1372,7 @@ class ZipFile: """ Class with methods to open, read, write, close, list zip files. z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True, - compresslevel=None, _ZipInfo=ZipInfo, _ZipExtFile=ZipExtFile) + compresslevel=None, zipinfo_class=ZipInfo, zipextfile_class=ZipExtFile) file: Either the path to the file, or a file-like object. If it is a path, the file will be opened and closed by ZipFile. @@ -1392,7 +1392,14 @@ class ZipFile: When using ZIP_ZSTANDARD integers -7 though 22 are common, see the CompressionParameter enum in compression.zstd for details. - _ZipInfo: A class that can replace ZipInfo. This is designed to help extend + zipinfo_class: A class that can replace ZipInfo. This is designed to help + extend ZipFile. + For example, to implement other encryption or compression + methods. + zipextfile_class: A class that can replace ZipExtFile. This is designed to + help extend ZipFile. + For example to implement other encryption or compression + methods. ZipFile, for example to implement other encryption or compression methods. This is private as there is no commitemnt to maintain backward @@ -1409,15 +1416,15 @@ class ZipFile: def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True, compresslevel=None, *, strict_timestamps=True, metadata_encoding=None, - _ZipInfo=ZipInfo, _ZipExtFile=ZipExtFile): + zipinfo_class=ZipInfo, zipextfile_class=ZipExtFile): """Open the ZIP file with mode read 'r', write 'w', exclusive create 'x', or append 'a'.""" if mode not in ('r', 'w', 'x', 'a'): raise ValueError("ZipFile requires mode 'r', 'w', 'x', or 'a'") _check_compression(compression) - self._ZipInfo = _ZipInfo - self._ZipExtFile = _ZipExtFile + self._ZipInfo = zipinfo_class + self._ZipExtFile = zipextfile_class self._allowZip64 = allowZip64 self._didModify = False self.debug = 0 # Level of printing: 0 through 3 From a6d22b741d4314e21a8670a4739240951aad8e65 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 27 Jul 2025 12:58:38 +0100 Subject: [PATCH 03/10] Update after review. --- Lib/zipfile/__init__.py | 16 +++++++--------- ...2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 52f87ac2d9d4624..7ef789a03805361 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -869,6 +869,10 @@ def _get_decompressor(compress_type): class _SharedFile: + """ + Protect an already opened member of the archive from being read or written + at the same time. + """ def __init__(self, file, pos, close, lock, writing): self._file = file self._pos = pos @@ -1400,15 +1404,9 @@ class ZipFile: help extend ZipFile. For example to implement other encryption or compression methods. - ZipFile, for example to implement other encryption or compression - methods. - This is private as there is no commitemnt to maintain backward - compatibitly. - _ZipExtFile: A class that can replace ZipExtFile. This is designed to help - extend ZipFile, for example to implement other encryption - or compression methods. - This is private as there is no commitemnt to maintain backward - compatibitly. + ZipFile, for example to implement other encryption or compression + methods. + """ fp = None # Set here since __del__ checks it diff --git a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst index 678c2273c9151c0..ff252480815fc53 100644 --- a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst +++ b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst @@ -1,2 +1,2 @@ -zipfile.ZipFile.__init__ was updated to make it easier to subclass and -extend it to support new encryption methods. +:class:`zipfile.ZipFile` was given the `zipinfo_class` and `zipextfile_class` to make it easier to subclass and +extend it. \ No newline at end of file From 36defbcf32fa14a3f85db85804f7af893302662a Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 27 Jul 2025 12:58:49 +0100 Subject: [PATCH 04/10] Add tests. --- Lib/test/test_zipfile/test_core.py | 102 +++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index c033059a515db68..d4e6987e08e3a44 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -5,6 +5,7 @@ import io import itertools import os +import pathlib import posixpath import stat import struct @@ -485,6 +486,9 @@ def tearDown(self): class StoredTestsWithSourceFile(AbstractTestsWithSourceFile, unittest.TestCase): + """ + Test in which the files inside the archive are not compressed. + """ compression = zipfile.ZIP_STORED test_low_compression = None @@ -676,6 +680,104 @@ def test_add_file_after_2107(self): self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 59)) + class CustomZipInfo(zipfile.ZipInfo): + """ + Support for testing extending and subclassing ZipFile. + """ + + class CustomZipExtFile(zipfile.ZipExtFile): + """ + Support for testing extending and subclassing ZipFile. + """ + + def test_read_custom_zipinfo_and_zipextfile(self): + """ + A subclass of ZipFile can be implemented to read and handle the + archive content using custom ZipInfo and ZipExtFile implementations. + """ + # Create the file using the default Zipfile. + source = io.BytesIO() + with zipfile.ZipFile(source, 'w', zipfile.ZIP_STORED) as zipfp: + zipfp.writestr('test.txt', 'some-text-content') + + with zipfile.ZipFile( + source, 'r', + zipinfo_class=self.CustomZipInfo, + zipextfile_class=self.CustomZipExtFile, + ) as zipfp: + # Archive content returns the custom ZipInfo + members = zipfp.infolist() + self.assertEqual(1, len(members)) + self.assertIsInstance(members[0], self.CustomZipInfo) + + # Archive members can be opened using the custom ZipInfo + target_member = members[0] + with zipfp.open(target_member, mode='r') as memberfp: + self.assertIsInstance(memberfp, self.CustomZipExtFile) + + def test_write_custom_zipinfo(self): + """ + A subclass of ZipFile can be implemented to write and handle the + archive content using custom ZipInfo implementation. + """ + destination = io.BytesIO() + with zipfile.ZipFile( + destination, 'w', zipinfo_class=self.CustomZipInfo) as zipfp: + # It can write using the specific custom classe. + new_member = self.CustomZipInfo('new-member.txt') + with zipfp.open(new_member, mode='w') as memberfp: + self.assertIs(new_member, memberfp._zinfo) + + # When creating a new member using just the name, + # the custom ZipInfo is used internally. + with zipfp.open('other-member.txt', mode='w') as memberfp: + self.assertIsInstance(memberfp._zinfo, self.CustomZipInfo) + self.assertIsInstance( + zipfp.NameToInfo['other-member.txt'], self.CustomZipInfo) + + # ZipFile.writestr can handle the custom class or just the + # archive name as text. + custom_member = self.CustomZipInfo('some-member.txt') + zipfp.writestr(custom_member, b'some-new-content') + zipfp.writestr('some-name.txt', b'other-content') + self.assertIsInstance( + zipfp.NameToInfo['some-name.txt'], self.CustomZipInfo) + + # ZipFile.mkdir can handle the custom class or just text. + custom_dir = self.CustomZipInfo('some-directory/') + custom_dir.CRC = 0 + zipfp.mkdir(custom_dir) + zipfp.mkdir('dir-as-text/') + self.assertIsInstance( + zipfp.NameToInfo['dir-as-text/'], self.CustomZipInfo) + + # When writing from an external file, the file is created using + # the custom ZipInfo + with temp_dir() as source_dir: + source_file = pathlib.Path(source_dir).joinpath('source.txt') + with open(source_file, 'wb') as fp: + fp.write(b'some-content') + zipfp.write(source_file, arcname='newly-file.txt') + self.assertIsInstance( + zipfp.NameToInfo['newly-file.txt'], self.CustomZipInfo) + + def test_extract_custom_zipinfo(self): + """ + A subclass of ZipFile can be implemented to extact the + archive content using custom ZipInfo implementation. + """ + destination = io.BytesIO() + with zipfile.ZipFile( + destination, 'w', zipinfo_class=self.CustomZipInfo) as zipfp: + zipfp.mkdir('dir-as-text/') + dir_info = zipfp.NameToInfo['dir-as-text/'] + self.assertIsInstance(dir_info, self.CustomZipInfo) + + with temp_dir() as extract_dir: + zipfp.extract(dir_info, path=extract_dir) + zipfp.extract('dir-as-text/', path=extract_dir) + + @requires_zlib() class DeflateTestsWithSourceFile(AbstractTestsWithSourceFile, unittest.TestCase): From a3be112135125e2e5e58780e76b93fe717eb4632 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 27 Jul 2025 13:05:20 +0100 Subject: [PATCH 05/10] Add what's new and fix sphinx lint. --- Doc/whatsnew/3.15.rst | 9 +++++++++ .../2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index e8e2c1ed6047bfe..caa7f01fe235655 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -329,6 +329,15 @@ unittest (Contributed by Garry Cairns in :gh:`134567`.) +zipfile +------- + +* :class:`zipfile.ZipFile` now has the ``zipinfo_class`` + and ``zipextfile_class`` init arguments make it easier to subclass and + extend it. + (Contributed by Adi Roiban in :gh:`81719`.) + + zlib ---- diff --git a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst index ff252480815fc53..5b44f66e8234de4 100644 --- a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst +++ b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst @@ -1,2 +1,2 @@ -:class:`zipfile.ZipFile` was given the `zipinfo_class` and `zipextfile_class` to make it easier to subclass and -extend it. \ No newline at end of file +:class:`zipfile.ZipFile` was given the ``zipinfo_class`` and ``zipextfile_class`` to make it easier to subclass and +extend it. From 2c998fb003385ebab9c6f0b5e4a8375fe99fc58f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 27 Jul 2025 13:24:02 +0100 Subject: [PATCH 06/10] Fix docstring. --- Lib/zipfile/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 7ef789a03805361..e5b04795e61d9f3 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -1404,9 +1404,6 @@ class ZipFile: help extend ZipFile. For example to implement other encryption or compression methods. - ZipFile, for example to implement other encryption or compression - methods. - """ fp = None # Set here since __del__ checks it From 63856e88701ad42dba0e3d77cef05993c386d13e Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 28 Jul 2025 13:53:56 +0100 Subject: [PATCH 07/10] Update tests --- Lib/test/test_zipfile/test_core.py | 46 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index d4e6987e08e3a44..f409d96c42f5dc8 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -5,7 +5,6 @@ import io import itertools import os -import pathlib import posixpath import stat import struct @@ -16,7 +15,7 @@ import unittest.mock as mock import zipfile - +from pathlib import Path from tempfile import TemporaryFile from random import randint, random, randbytes @@ -486,9 +485,6 @@ def tearDown(self): class StoredTestsWithSourceFile(AbstractTestsWithSourceFile, unittest.TestCase): - """ - Test in which the files inside the archive are not compressed. - """ compression = zipfile.ZIP_STORED test_low_compression = None @@ -679,16 +675,11 @@ def test_add_file_after_2107(self): zinfo = zipfp.getinfo(TESTFN) self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 59)) - class CustomZipInfo(zipfile.ZipInfo): - """ - Support for testing extending and subclassing ZipFile. - """ + pass class CustomZipExtFile(zipfile.ZipExtFile): - """ - Support for testing extending and subclassing ZipFile. - """ + pass def test_read_custom_zipinfo_and_zipextfile(self): """ @@ -699,6 +690,7 @@ def test_read_custom_zipinfo_and_zipextfile(self): source = io.BytesIO() with zipfile.ZipFile(source, 'w', zipfile.ZIP_STORED) as zipfp: zipfp.writestr('test.txt', 'some-text-content') + source.seek(0) with zipfile.ZipFile( source, 'r', @@ -714,6 +706,7 @@ def test_read_custom_zipinfo_and_zipextfile(self): target_member = members[0] with zipfp.open(target_member, mode='r') as memberfp: self.assertIsInstance(memberfp, self.CustomZipExtFile) + self.assertEqual(b'some-text-content', memberfp.read()) def test_write_custom_zipinfo(self): """ @@ -723,7 +716,7 @@ def test_write_custom_zipinfo(self): destination = io.BytesIO() with zipfile.ZipFile( destination, 'w', zipinfo_class=self.CustomZipInfo) as zipfp: - # It can write using the specific custom classe. + # It can write using the specific custom class. new_member = self.CustomZipInfo('new-member.txt') with zipfp.open(new_member, mode='w') as memberfp: self.assertIs(new_member, memberfp._zinfo) @@ -731,7 +724,7 @@ def test_write_custom_zipinfo(self): # When creating a new member using just the name, # the custom ZipInfo is used internally. with zipfp.open('other-member.txt', mode='w') as memberfp: - self.assertIsInstance(memberfp._zinfo, self.CustomZipInfo) + memberfp.write(b'some-content') self.assertIsInstance( zipfp.NameToInfo['other-member.txt'], self.CustomZipInfo) @@ -754,7 +747,7 @@ def test_write_custom_zipinfo(self): # When writing from an external file, the file is created using # the custom ZipInfo with temp_dir() as source_dir: - source_file = pathlib.Path(source_dir).joinpath('source.txt') + source_file = Path(source_dir) / 'source.txt' with open(source_file, 'wb') as fp: fp.write(b'some-content') zipfp.write(source_file, arcname='newly-file.txt') @@ -766,16 +759,27 @@ def test_extract_custom_zipinfo(self): A subclass of ZipFile can be implemented to extact the archive content using custom ZipInfo implementation. """ + destination = io.BytesIO() - with zipfile.ZipFile( - destination, 'w', zipinfo_class=self.CustomZipInfo) as zipfp: + with zipfile.ZipFile(destination, 'w') as zipfp: zipfp.mkdir('dir-as-text/') - dir_info = zipfp.NameToInfo['dir-as-text/'] - self.assertIsInstance(dir_info, self.CustomZipInfo) + zipfp.writestr('test.txt', b'new file content') + destination.seek(0) + with zipfile.ZipFile( + destination, 'r', zipinfo_class=self.CustomZipInfo) as zipfp: with temp_dir() as extract_dir: - zipfp.extract(dir_info, path=extract_dir) - zipfp.extract('dir-as-text/', path=extract_dir) + expected_dir = Path(extract_dir) / 'dir-as-text' + expected_file = Path(extract_dir) / 'test.txt' + + # Check extracting using custom ZipInfo + dir_info = zipfp.NameToInfo['dir-as-text/'] + #zipfp.extract(dir_info, path=extract_dir) + #self.assertTrue(expected_dir.is_dir()) + # Check extracting using file name. + zipfp.extract('test.txt', path=extract_dir) + with expected_file.open('rb') as fp: + self.assertEqual(b'new file content', fp.read()) @requires_zlib() From d7baea157e4b8021c061b2915de1331d5ba29875 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 28 Jul 2025 14:53:20 +0100 Subject: [PATCH 08/10] Add documentation --- Doc/library/zipfile.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index a1261ec471c92ef..762c65d3d1e4ecf 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -171,7 +171,8 @@ ZipFile Objects .. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True, \ compresslevel=None, *, strict_timestamps=True, \ - metadata_encoding=None) + metadata_encoding=None, \ + zipinfo_class=ZipInfo, zipextfile_class=ZipExtFile) Open a ZIP file, where *file* can be a path to a file (a string), a file-like object or a :term:`path-like object`. @@ -235,6 +236,12 @@ ZipFile Objects with ZipFile('spam.zip', 'w') as myzip: myzip.write('eggs.txt') + The *zipinfo_class* and *zipextfile_class* are designed to help extend the + functionalities. + For now, there is no stable public protocol or interface for these classes. + When implementing custom classes you will need to follow the public and + private API from :class:`ZipInfo` and ``ZipExtFile``. + .. note:: *metadata_encoding* is an instance-wide setting for the ZipFile. @@ -278,6 +285,9 @@ ZipFile Objects Added support for specifying member name encoding for reading metadata in the zipfile's directory and file headers. + .. versionchanged:: 3.15 + Add the *zipinfo_class* and *zipextfile_class* parameters. + .. method:: ZipFile.close() From 83e79796851269dee32c8eb6f54f02252d010108 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 28 Jul 2025 16:14:33 +0100 Subject: [PATCH 09/10] Update after review from Brian --- Doc/library/zipfile.rst | 13 +++++-------- .../2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst | 3 +-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 762c65d3d1e4ecf..1d803b7b1fc046c 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -229,6 +229,9 @@ ZipFile Objects :meth:`closed ` without adding any files to the archive, the appropriate ZIP structures for an empty archive will be written to the file. + The *zipinfo_class* and *zipextfile_class* arguments can be used to replace + the default :class:`ZipInfo` and :class:`!ZipExtFile` classes with different ones. + ZipFile is also a context manager and therefore supports the :keyword:`with` statement. In the example, *myzip* is closed after the :keyword:`!with` statement's suite is finished---even if an exception occurs:: @@ -236,12 +239,6 @@ ZipFile Objects with ZipFile('spam.zip', 'w') as myzip: myzip.write('eggs.txt') - The *zipinfo_class* and *zipextfile_class* are designed to help extend the - functionalities. - For now, there is no stable public protocol or interface for these classes. - When implementing custom classes you will need to follow the public and - private API from :class:`ZipInfo` and ``ZipExtFile``. - .. note:: *metadata_encoding* is an instance-wide setting for the ZipFile. @@ -285,8 +282,8 @@ ZipFile Objects Added support for specifying member name encoding for reading metadata in the zipfile's directory and file headers. - .. versionchanged:: 3.15 - Add the *zipinfo_class* and *zipextfile_class* parameters. + .. versionchanged:: next + Added the *zipinfo_class* and *zipextfile_class* parameters. .. method:: ZipFile.close() diff --git a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst index 5b44f66e8234de4..b79aafba6a06c86 100644 --- a/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst +++ b/Misc/NEWS.d/next/Library/2025-07-25-14-27-29.gh-issue-81719.hWp7Mn.rst @@ -1,2 +1 @@ -:class:`zipfile.ZipFile` was given the ``zipinfo_class`` and ``zipextfile_class`` to make it easier to subclass and -extend it. +:class:`zipfile.ZipFile` was given the *zipinfo_class* and *zipextfile_class* to make it easier to subclass and extend it. From 7ac534a2da6962843a0855ef13a19a3747bd5bf6 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 28 Jul 2025 16:15:34 +0100 Subject: [PATCH 10/10] Another feedback from Brian. --- Doc/whatsnew/3.15.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index caa7f01fe235655..908f5fd08ae0123 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -332,9 +332,8 @@ unittest zipfile ------- -* :class:`zipfile.ZipFile` now has the ``zipinfo_class`` - and ``zipextfile_class`` init arguments make it easier to subclass and - extend it. +* :class:`zipfile.ZipFile` now accepts the keyword-only arguments *zipinfo_class* + and *zipextfile_class* to make it easier to subclass and extend. (Contributed by Adi Roiban in :gh:`81719`.)