summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/zipfile.rst11
-rw-r--r--Doc/whatsnew/3.14.rst8
-rw-r--r--Lib/test/test_zipfile/_path/test_path.py19
-rw-r--r--Lib/test/test_zipfile/test_core.py29
-rw-r--r--Lib/zipfile/__init__.py33
-rw-r--r--Misc/NEWS.d/next/Library/2024-08-28-16-10-37.gh-issue-123424.u96_i6.rst1
6 files changed, 72 insertions, 29 deletions
diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst
index 5583c6b..afe1cd5 100644
--- a/Doc/library/zipfile.rst
+++ b/Doc/library/zipfile.rst
@@ -84,6 +84,17 @@ The module defines the following items:
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,
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 2767fd3..53415bb 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -661,6 +661,14 @@ uuid
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.
diff --git a/Lib/test/test_zipfile/_path/test_path.py b/Lib/test/test_zipfile/_path/test_path.py
index aba5155..1ee45f5 100644
--- a/Lib/test/test_zipfile/_path/test_path.py
+++ b/Lib/test/test_zipfile/_path/test_path.py
@@ -634,7 +634,7 @@ class TestPath(unittest.TestCase):
"""
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()
@@ -657,20 +657,3 @@ class DirtyZipInfo(zipfile.ZipInfo):
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
diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py
index 124e088..49f39b9 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 posixpath
+import stat
import struct
import subprocess
import sys
@@ -2211,6 +2212,34 @@ class OtherTests(unittest.TestCase):
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()
diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py
index f4d396a..052ef47 100644
--- a/Lib/zipfile/__init__.py
+++ b/Lib/zipfile/__init__.py
@@ -13,6 +13,7 @@ import struct
import sys
import threading
import time
+from typing import Self
try:
import zlib # We may need its compression method
@@ -605,6 +606,24 @@ class ZipInfo:
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('/'):
@@ -1908,18 +1927,10 @@ class ZipFile:
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(
diff --git a/Misc/NEWS.d/next/Library/2024-08-28-16-10-37.gh-issue-123424.u96_i6.rst b/Misc/NEWS.d/next/Library/2024-08-28-16-10-37.gh-issue-123424.u96_i6.rst
new file mode 100644
index 0000000..4df4bbf
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-08-28-16-10-37.gh-issue-123424.u96_i6.rst
@@ -0,0 +1 @@
+Add :meth:`zipfile.ZipInfo._for_archive` setting default properties on :class:`~zipfile.ZipInfo` objects. Patch by Bénédikt Tran and Jason R. Coombs.