From 6341b835b34ecc1af4d5292356d15ee116be5c71 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Mon, 28 Mar 2022 20:14:53 +0100 Subject: [PATCH 1/8] Add ZipFile.mkdir --- Doc/library/zipfile.rst | 12 +++++++++ Lib/test/test_zipfile.py | 32 ++++++++++++++++++++++++ Lib/zipfile.py | 53 +++++++++++++++++++++++++++------------- a.zip | 0 4 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 a.zip diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index bfcc883de69271..5accf2d682e729 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -478,6 +478,18 @@ ZipFile Objects a closed ZipFile will raise a :exc:`ValueError`. Previously, a :exc:`RuntimeError` was raised. +.. method:: ZipFile.mkdir(zinfo_or_directory, mode=511) + + .. versionadded:: 3.11 + + Create a directory inside the archive. If *zinfo_or_directory* is a string, + a directory is created inside the archive with the mode that is specified in + the *mode* argument. If, however, *zinfo_or_directory* is + a :class:`ZipInfo` instance then the *mode* argument is ignored. + + The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``. + + The following data attributes are also available: diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 26c40457e62a05..999a2d90aeff4f 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2637,6 +2637,38 @@ def test_writestr_dir(self): self.assertTrue(os.path.isdir(os.path.join(target, "x"))) self.assertEqual(os.listdir(target), ["x"]) + def test_mkdir(self): + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.mkdir("directory") + zinfo = zf.filelist[0] + self.assertEqual(zinfo.filename, "directory/") + self.assertEqual(zinfo.external_attr, (511 << 16) | 0x10) + + zf.mkdir("directory2/") + zinfo = zf.filelist[1] + self.assertEqual(zinfo.filename, "directory2/") + self.assertEqual(zinfo.external_attr, (511 << 16) | 0x10) + + zf.mkdir("directory3", mode=777) + zinfo = zf.filelist[2] + self.assertEqual(zinfo.filename, "directory3/") + self.assertEqual(zinfo.external_attr, (777 << 16) | 0x10) + + old_zinfo = zipfile.ZipInfo("directory4/") + old_zinfo.external_attr = (511 << 16) | 0x10 + old_zinfo.CRC = 0 + old_zinfo.file_size = 0 + old_zinfo.compress_size = 0 + zf.mkdir(old_zinfo, mode=777) + new_zinfo = zf.filelist[3] + self.assertEqual(old_zinfo.filename, "directory4/") + self.assertEqual(old_zinfo.external_attr, new_zinfo.external_attr) + + target = os.path.join(TESTFN2, "target") + os.mkdir(target) + zf.extractall(target) + self.assertEqual(os.listdir(target), ["directory", "directory2", "directory3", "directory4"]) + def tearDown(self): rmtree(TESTFN2) if os.path.exists(TESTFN): diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 721834aff13a74..ff3cd64f0a9b29 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -1772,6 +1772,7 @@ def write(self, filename, arcname=None, if zinfo.is_dir(): zinfo.compress_size = 0 zinfo.CRC = 0 + self.mkdir(zinfo) else: if compress_type is not None: zinfo.compress_type = compress_type @@ -1783,23 +1784,6 @@ def write(self, filename, arcname=None, else: zinfo._compresslevel = self.compresslevel - if zinfo.is_dir(): - with self._lock: - if self._seekable: - self.fp.seek(self.start_dir) - zinfo.header_offset = self.fp.tell() # Start of header bytes - if zinfo.compress_type == ZIP_LZMA: - # Compressed data includes an end-of-stream (EOS) marker - zinfo.flag_bits |= _MASK_COMPRESS_OPTION_1 - - self._writecheck(zinfo) - self._didModify = True - - self.filelist.append(zinfo) - self.NameToInfo[zinfo.filename] = zinfo - self.fp.write(zinfo.FileHeader(False)) - self.start_dir = self.fp.tell() - else: with open(filename, "rb") as src, self.open(zinfo, 'w') as dest: shutil.copyfileobj(src, dest, 1024*8) @@ -1844,6 +1828,41 @@ def writestr(self, zinfo_or_arcname, data, with self.open(zinfo, mode='w') as dest: dest.write(data) + def mkdir(self, zinfo_or_directory_name, mode=511): + """Creates a directory inside the zip archive.""" + if isinstance(zinfo_or_directory_name, ZipInfo): + zinfo = zinfo_or_directory_name + if not zinfo.is_dir(): + raise ValueError("The given ZipInfo does not describe a directory") + elif isinstance(zinfo_or_directory_name, str): + directory_name = zinfo_or_directory_name + if not directory_name.endswith("/"): + directory_name += "/" + zinfo = ZipInfo(directory_name) + zinfo.compress_size = 0 + zinfo.CRC = 0 + zinfo.external_attr = (mode & 0xFFFF) << 16 + zinfo.file_size = 0 + zinfo.external_attr |= 0x10 + else: + raise TypeError("Expected type str or ZipInfo") + + with self._lock: + if self._seekable: + self.fp.seek(self.start_dir) + zinfo.header_offset = self.fp.tell() # Start of header bytes + if zinfo.compress_type == ZIP_LZMA: + # Compressed data includes an end-of-stream (EOS) marker + zinfo.flag_bits |= _MASK_COMPRESS_OPTION_1 + + self._writecheck(zinfo) + self._didModify = True + + self.filelist.append(zinfo) + self.NameToInfo[zinfo.filename] = zinfo + self.fp.write(zinfo.FileHeader(False)) + self.start_dir = self.fp.tell() + def __del__(self): """Call the "close()" method in case the user forgot.""" self.close() diff --git a/a.zip b/a.zip new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 5c441ad73d695fc5ad58c2f6774275bfcbcd19cb Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Mon, 28 Mar 2022 20:17:16 +0100 Subject: [PATCH 2/8] Add news entry --- Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst diff --git a/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst b/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst new file mode 100644 index 00000000000000..8dd98e592daedf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst @@ -0,0 +1 @@ +Add ZipFile.mkdir From cc3c76eb6845961862fa2aa237ffe52bb798fc7d Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Mon, 28 Mar 2022 20:33:35 +0100 Subject: [PATCH 3/8] Remove example zip file --- a.zip | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 a.zip diff --git a/a.zip b/a.zip deleted file mode 100644 index e69de29bb2d1d6..00000000000000 From 1028df20dcc07db2cb4eb576808dd37b62578bf8 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Mon, 28 Mar 2022 20:53:24 +0100 Subject: [PATCH 4/8] Allow directory ordering to vary in tests --- Lib/test/test_zipfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 999a2d90aeff4f..cb0c18039e948b 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2667,7 +2667,7 @@ def test_mkdir(self): target = os.path.join(TESTFN2, "target") os.mkdir(target) zf.extractall(target) - self.assertEqual(os.listdir(target), ["directory", "directory2", "directory3", "directory4"]) + self.assertEqual(set(os.listdir(target)), {"directory", "directory2", "directory3", "directory4"}) def tearDown(self): rmtree(TESTFN2) From 702f0d89492af36ba1aae8b66ac32e5acbdaebb5 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Tue, 29 Mar 2022 15:37:39 +0100 Subject: [PATCH 5/8] Move version directive to end of block --- Doc/library/zipfile.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index 5accf2d682e729..d6a1fce49c545e 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -480,8 +480,6 @@ ZipFile Objects .. method:: ZipFile.mkdir(zinfo_or_directory, mode=511) - .. versionadded:: 3.11 - Create a directory inside the archive. If *zinfo_or_directory* is a string, a directory is created inside the archive with the mode that is specified in the *mode* argument. If, however, *zinfo_or_directory* is @@ -489,6 +487,7 @@ ZipFile Objects The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``. + .. versionadded:: 3.11 The following data attributes are also available: From 4a93d152313e55819d50c6c1106c735895b70127 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Fri, 1 Apr 2022 04:40:15 +0100 Subject: [PATCH 6/8] Add fille type flag to ZipFile.mkdir, add ZipFIle.write directory tests --- Lib/test/test_zipfile.py | 33 +++++++++++++++++++++++++++------ Lib/zipfile.py | 2 +- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index cb0c18039e948b..9098e11683ae01 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2642,24 +2642,24 @@ def test_mkdir(self): zf.mkdir("directory") zinfo = zf.filelist[0] self.assertEqual(zinfo.filename, "directory/") - self.assertEqual(zinfo.external_attr, (511 << 16) | 0x10) + self.assertEqual(zinfo.external_attr, (0o40777 << 16) | 0x10) zf.mkdir("directory2/") zinfo = zf.filelist[1] self.assertEqual(zinfo.filename, "directory2/") - self.assertEqual(zinfo.external_attr, (511 << 16) | 0x10) + self.assertEqual(zinfo.external_attr, (0o40777 << 16) | 0x10) - zf.mkdir("directory3", mode=777) + zf.mkdir("directory3", mode=0o777) zinfo = zf.filelist[2] self.assertEqual(zinfo.filename, "directory3/") - self.assertEqual(zinfo.external_attr, (777 << 16) | 0x10) + self.assertEqual(zinfo.external_attr, (0o40777 << 16) | 0x10) old_zinfo = zipfile.ZipInfo("directory4/") - old_zinfo.external_attr = (511 << 16) | 0x10 + old_zinfo.external_attr = (0o40777 << 16) | 0x10 old_zinfo.CRC = 0 old_zinfo.file_size = 0 old_zinfo.compress_size = 0 - zf.mkdir(old_zinfo, mode=777) + zf.mkdir(old_zinfo) new_zinfo = zf.filelist[3] self.assertEqual(old_zinfo.filename, "directory4/") self.assertEqual(old_zinfo.external_attr, new_zinfo.external_attr) @@ -2669,6 +2669,27 @@ def test_mkdir(self): zf.extractall(target) self.assertEqual(set(os.listdir(target)), {"directory", "directory2", "directory3", "directory4"}) + def test_create_directory_with_write(self): + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr(zipfile.ZipInfo('directory/'), '') + + zinfo = zf.filelist[0] + self.assertEqual(zinfo.filename, "directory/") + + directory = os.path.join(TESTFN2, "directory2") + os.mkdir(directory) + mode = os.stat(directory).st_mode + zf.write(directory, arcname="directory2/") + zinfo = zf.filelist[1] + self.assertEqual(zinfo.filename, "directory2/") + self.assertEqual(zinfo.external_attr, ((0o40000 | mode) << 16) | 0x10) + + target = os.path.join(TESTFN2, "target") + os.mkdir(target) + zf.extractall(target) + + self.assertEqual(set(os.listdir(target)), {"directory", "directory2"}) + def tearDown(self): rmtree(TESTFN2) if os.path.exists(TESTFN): diff --git a/Lib/zipfile.py b/Lib/zipfile.py index ff3cd64f0a9b29..dc02011084329a 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -1841,7 +1841,7 @@ def mkdir(self, zinfo_or_directory_name, mode=511): zinfo = ZipInfo(directory_name) zinfo.compress_size = 0 zinfo.CRC = 0 - zinfo.external_attr = (mode & 0xFFFF) << 16 + zinfo.external_attr = ((0o40000 | mode) & 0xFFFF) << 16 zinfo.file_size = 0 zinfo.external_attr |= 0x10 else: From 0b4877a43f1b70c6a17d0f51c739be92bf063730 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Fri, 1 Apr 2022 05:15:56 +0100 Subject: [PATCH 7/8] Remove redundant file type mask application --- Lib/test/test_zipfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 9098e11683ae01..17111b3a40fef9 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2682,7 +2682,7 @@ def test_create_directory_with_write(self): zf.write(directory, arcname="directory2/") zinfo = zf.filelist[1] self.assertEqual(zinfo.filename, "directory2/") - self.assertEqual(zinfo.external_attr, ((0o40000 | mode) << 16) | 0x10) + self.assertEqual(zinfo.external_attr, (mode << 16) | 0x10) target = os.path.join(TESTFN2, "target") os.mkdir(target) From 0387bfb3c8d73432fe49d6eaa4eb90fa1e2e7ef4 Mon Sep 17 00:00:00 2001 From: Sam Ezeh Date: Fri, 1 Apr 2022 21:42:42 +0100 Subject: [PATCH 8/8] Amend new entry --- .../NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst b/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst index 8dd98e592daedf..7696091221cb5a 100644 --- a/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst +++ b/Misc/NEWS.d/next/Library/2022-03-28-20-16-37.bpo-4833.2vSUE5.rst @@ -1 +1 @@ -Add ZipFile.mkdir +Add :meth:`ZipFile.mkdir`