formerly protected :attr:`!_compresslevel`. The older protected name
continues to work as a property for backwards compatibility.
+
+ .. method:: _for_archive(archive)
+
+ Resolve the date_time, compression attributes, and external attributes
+ to suitable defaults as used by :meth:`ZipFile.writestr`.
+
+ Returns self for chaining.
+
+ .. versionadded:: 3.14
+
+
.. function:: is_zipfile(filename)
Returns ``True`` if *filename* is a valid ZIP file based on its magic number,
in :rfc:`9562`.
(Contributed by Bénédikt Tran in :gh:`89083`.)
+zipinfo
+-------
+
+* Added :func:`ZipInfo._for_archive <zipfile.ZipInfo._for_archive>`
+ to resolve suitable defaults for a :class:`~zipfile.ZipInfo` object
+ as used by :func:`ZipFile.writestr <zipfile.ZipFile.writestr>`.
+
+ (Contributed by Bénédikt Tran in :gh:`123424`.)
.. Add improved modules above alphabetically, not here at the end.
"""
data = io.BytesIO()
zf = zipfile.ZipFile(data, "w")
- zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
+ zf.writestr(DirtyZipInfo("foo\\bar")._for_archive(zf), b"content")
zf.filename = ''
root = zipfile.Path(zf)
(first,) = root.iterdir()
def __init__(self, filename, *args, **kwargs):
super().__init__(filename, *args, **kwargs)
self.filename = filename
-
- @classmethod
- def for_name(cls, name, archive):
- """
- Construct the same way that ZipFile.writestr does.
-
- TODO: extract this functionality and re-use
- """
- self = cls(filename=name, date_time=time.localtime(time.time())[:6])
- self.compress_type = archive.compression
- self.compress_level = archive.compresslevel
- if self.filename.endswith('/'): # pragma: no cover
- self.external_attr = 0o40775 << 16 # drwxrwxr-x
- self.external_attr |= 0x10 # MS-DOS directory flag
- else:
- self.external_attr = 0o600 << 16 # ?rw-------
- return self
import itertools
import os
import posixpath
+import stat
import struct
import subprocess
import sys
zi = zipfile.ZipInfo(filename="empty")
self.assertEqual(repr(zi), "<ZipInfo filename='empty' file_size=0>")
+ def test_for_archive(self):
+ base_filename = TESTFN2.rstrip('/')
+
+ with zipfile.ZipFile(TESTFN, mode="w", compresslevel=1,
+ compression=zipfile.ZIP_STORED) as zf:
+ # no trailing forward slash
+ zi = zipfile.ZipInfo(base_filename)._for_archive(zf)
+ self.assertEqual(zi.compress_level, 1)
+ self.assertEqual(zi.compress_type, zipfile.ZIP_STORED)
+ # ?rw- --- ---
+ filemode = stat.S_IRUSR | stat.S_IWUSR
+ # filemode is stored as the highest 16 bits of external_attr
+ self.assertEqual(zi.external_attr >> 16, filemode)
+ self.assertEqual(zi.external_attr & 0xFF, 0) # no MS-DOS flag
+
+ with zipfile.ZipFile(TESTFN, mode="w", compresslevel=1,
+ compression=zipfile.ZIP_STORED) as zf:
+ # with a trailing slash
+ zi = zipfile.ZipInfo(f'{base_filename}/')._for_archive(zf)
+ self.assertEqual(zi.compress_level, 1)
+ self.assertEqual(zi.compress_type, zipfile.ZIP_STORED)
+ # d rwx rwx r-x
+ filemode = stat.S_IFDIR
+ filemode |= stat.S_IRWXU | stat.S_IRWXG
+ filemode |= stat.S_IROTH | stat.S_IXOTH
+ self.assertEqual(zi.external_attr >> 16, filemode)
+ self.assertEqual(zi.external_attr & 0xFF, 0x10) # MS-DOS flag
+
def test_create_empty_zipinfo_default_attributes(self):
"""Ensure all required attributes are set."""
zi = zipfile.ZipInfo()
import sys
import threading
import time
+from typing import Self
try:
import zlib # We may need its compression method
return zinfo
+ def _for_archive(self, archive: ZipFile) -> Self:
+ """Resolve suitable defaults from the archive.
+
+ Resolve the date_time, compression attributes, and external attributes
+ to suitable defaults as used by :method:`ZipFile.writestr`.
+
+ Return self.
+ """
+ self.date_time = time.localtime(time.time())[:6]
+ self.compress_type = archive.compression
+ self.compress_level = archive.compresslevel
+ if self.filename.endswith('/'): # pragma: no cover
+ self.external_attr = 0o40775 << 16 # drwxrwxr-x
+ self.external_attr |= 0x10 # MS-DOS directory flag
+ else:
+ self.external_attr = 0o600 << 16 # ?rw-------
+ return self
+
def is_dir(self):
"""Return True if this archive member is a directory."""
if self.filename.endswith('/'):
the name of the file in the archive."""
if isinstance(data, str):
data = data.encode("utf-8")
- if not isinstance(zinfo_or_arcname, ZipInfo):
- zinfo = ZipInfo(filename=zinfo_or_arcname,
- date_time=time.localtime(time.time())[:6])
- zinfo.compress_type = self.compression
- zinfo.compress_level = self.compresslevel
- if zinfo.filename.endswith('/'):
- zinfo.external_attr = 0o40775 << 16 # drwxrwxr-x
- zinfo.external_attr |= 0x10 # MS-DOS directory flag
- else:
- zinfo.external_attr = 0o600 << 16 # ?rw-------
- else:
+ if isinstance(zinfo_or_arcname, ZipInfo):
zinfo = zinfo_or_arcname
+ else:
+ zinfo = ZipInfo(zinfo_or_arcname)._for_archive(self)
if not self.fp:
raise ValueError(
--- /dev/null
+Add :meth:`zipfile.ZipInfo._for_archive` setting default properties on :class:`~zipfile.ZipInfo` objects. Patch by Bénédikt Tran and Jason R. Coombs.