Skip to content

Commit 050a8f9

Browse files
authored
bpo-4833: Add ZipFile.mkdir (GH-32160)
1 parent 9e88b57 commit 050a8f9

File tree

4 files changed

+101
-17
lines changed

4 files changed

+101
-17
lines changed

Doc/library/zipfile.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,17 @@ ZipFile Objects
478478
a closed ZipFile will raise a :exc:`ValueError`. Previously,
479479
a :exc:`RuntimeError` was raised.
480480

481+
.. method:: ZipFile.mkdir(zinfo_or_directory, mode=511)
482+
483+
Create a directory inside the archive. If *zinfo_or_directory* is a string,
484+
a directory is created inside the archive with the mode that is specified in
485+
the *mode* argument. If, however, *zinfo_or_directory* is
486+
a :class:`ZipInfo` instance then the *mode* argument is ignored.
487+
488+
The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``.
489+
490+
.. versionadded:: 3.11
491+
481492

482493
The following data attributes are also available:
483494

Lib/test/test_zipfile.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2637,6 +2637,59 @@ def test_writestr_dir(self):
26372637
self.assertTrue(os.path.isdir(os.path.join(target, "x")))
26382638
self.assertEqual(os.listdir(target), ["x"])
26392639

2640+
def test_mkdir(self):
2641+
with zipfile.ZipFile(TESTFN, "w") as zf:
2642+
zf.mkdir("directory")
2643+
zinfo = zf.filelist[0]
2644+
self.assertEqual(zinfo.filename, "directory/")
2645+
self.assertEqual(zinfo.external_attr, (0o40777 << 16) | 0x10)
2646+
2647+
zf.mkdir("directory2/")
2648+
zinfo = zf.filelist[1]
2649+
self.assertEqual(zinfo.filename, "directory2/")
2650+
self.assertEqual(zinfo.external_attr, (0o40777 << 16) | 0x10)
2651+
2652+
zf.mkdir("directory3", mode=0o777)
2653+
zinfo = zf.filelist[2]
2654+
self.assertEqual(zinfo.filename, "directory3/")
2655+
self.assertEqual(zinfo.external_attr, (0o40777 << 16) | 0x10)
2656+
2657+
old_zinfo = zipfile.ZipInfo("directory4/")
2658+
old_zinfo.external_attr = (0o40777 << 16) | 0x10
2659+
old_zinfo.CRC = 0
2660+
old_zinfo.file_size = 0
2661+
old_zinfo.compress_size = 0
2662+
zf.mkdir(old_zinfo)
2663+
new_zinfo = zf.filelist[3]
2664+
self.assertEqual(old_zinfo.filename, "directory4/")
2665+
self.assertEqual(old_zinfo.external_attr, new_zinfo.external_attr)
2666+
2667+
target = os.path.join(TESTFN2, "target")
2668+
os.mkdir(target)
2669+
zf.extractall(target)
2670+
self.assertEqual(set(os.listdir(target)), {"directory", "directory2", "directory3", "directory4"})
2671+
2672+
def test_create_directory_with_write(self):
2673+
with zipfile.ZipFile(TESTFN, "w") as zf:
2674+
zf.writestr(zipfile.ZipInfo('directory/'), '')
2675+
2676+
zinfo = zf.filelist[0]
2677+
self.assertEqual(zinfo.filename, "directory/")
2678+
2679+
directory = os.path.join(TESTFN2, "directory2")
2680+
os.mkdir(directory)
2681+
mode = os.stat(directory).st_mode
2682+
zf.write(directory, arcname="directory2/")
2683+
zinfo = zf.filelist[1]
2684+
self.assertEqual(zinfo.filename, "directory2/")
2685+
self.assertEqual(zinfo.external_attr, (mode << 16) | 0x10)
2686+
2687+
target = os.path.join(TESTFN2, "target")
2688+
os.mkdir(target)
2689+
zf.extractall(target)
2690+
2691+
self.assertEqual(set(os.listdir(target)), {"directory", "directory2"})
2692+
26402693
def tearDown(self):
26412694
rmtree(TESTFN2)
26422695
if os.path.exists(TESTFN):

Lib/zipfile.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,7 @@ def write(self, filename, arcname=None,
17721772
if zinfo.is_dir():
17731773
zinfo.compress_size = 0
17741774
zinfo.CRC = 0
1775+
self.mkdir(zinfo)
17751776
else:
17761777
if compress_type is not None:
17771778
zinfo.compress_type = compress_type
@@ -1783,23 +1784,6 @@ def write(self, filename, arcname=None,
17831784
else:
17841785
zinfo._compresslevel = self.compresslevel
17851786

1786-
if zinfo.is_dir():
1787-
with self._lock:
1788-
if self._seekable:
1789-
self.fp.seek(self.start_dir)
1790-
zinfo.header_offset = self.fp.tell() # Start of header bytes
1791-
if zinfo.compress_type == ZIP_LZMA:
1792-
# Compressed data includes an end-of-stream (EOS) marker
1793-
zinfo.flag_bits |= _MASK_COMPRESS_OPTION_1
1794-
1795-
self._writecheck(zinfo)
1796-
self._didModify = True
1797-
1798-
self.filelist.append(zinfo)
1799-
self.NameToInfo[zinfo.filename] = zinfo
1800-
self.fp.write(zinfo.FileHeader(False))
1801-
self.start_dir = self.fp.tell()
1802-
else:
18031787
with open(filename, "rb") as src, self.open(zinfo, 'w') as dest:
18041788
shutil.copyfileobj(src, dest, 1024*8)
18051789

@@ -1844,6 +1828,41 @@ def writestr(self, zinfo_or_arcname, data,
18441828
with self.open(zinfo, mode='w') as dest:
18451829
dest.write(data)
18461830

1831+
def mkdir(self, zinfo_or_directory_name, mode=511):
1832+
"""Creates a directory inside the zip archive."""
1833+
if isinstance(zinfo_or_directory_name, ZipInfo):
1834+
zinfo = zinfo_or_directory_name
1835+
if not zinfo.is_dir():
1836+
raise ValueError("The given ZipInfo does not describe a directory")
1837+
elif isinstance(zinfo_or_directory_name, str):
1838+
directory_name = zinfo_or_directory_name
1839+
if not directory_name.endswith("/"):
1840+
directory_name += "/"
1841+
zinfo = ZipInfo(directory_name)
1842+
zinfo.compress_size = 0
1843+
zinfo.CRC = 0
1844+
zinfo.external_attr = ((0o40000 | mode) & 0xFFFF) << 16
1845+
zinfo.file_size = 0
1846+
zinfo.external_attr |= 0x10
1847+
else:
1848+
raise TypeError("Expected type str or ZipInfo")
1849+
1850+
with self._lock:
1851+
if self._seekable:
1852+
self.fp.seek(self.start_dir)
1853+
zinfo.header_offset = self.fp.tell() # Start of header bytes
1854+
if zinfo.compress_type == ZIP_LZMA:
1855+
# Compressed data includes an end-of-stream (EOS) marker
1856+
zinfo.flag_bits |= _MASK_COMPRESS_OPTION_1
1857+
1858+
self._writecheck(zinfo)
1859+
self._didModify = True
1860+
1861+
self.filelist.append(zinfo)
1862+
self.NameToInfo[zinfo.filename] = zinfo
1863+
self.fp.write(zinfo.FileHeader(False))
1864+
self.start_dir = self.fp.tell()
1865+
18471866
def __del__(self):
18481867
"""Call the "close()" method in case the user forgot."""
18491868
self.close()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add :meth:`ZipFile.mkdir`

0 commit comments

Comments
 (0)