Skip to content

Commit 7e819ce

Browse files
picnixzjaraco
andauthored
gh-123424: add ZipInfo._for_archive to set suitable default properties (#123429)
--------- Co-authored-by: Jason R. Coombs <[email protected]>
1 parent ffece55 commit 7e819ce

File tree

6 files changed

+72
-29
lines changed

6 files changed

+72
-29
lines changed

Doc/library/zipfile.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ The module defines the following items:
8484
formerly protected :attr:`!_compresslevel`. The older protected name
8585
continues to work as a property for backwards compatibility.
8686

87+
88+
.. method:: _for_archive(archive)
89+
90+
Resolve the date_time, compression attributes, and external attributes
91+
to suitable defaults as used by :meth:`ZipFile.writestr`.
92+
93+
Returns self for chaining.
94+
95+
.. versionadded:: 3.14
96+
97+
8798
.. function:: is_zipfile(filename)
8899

89100
Returns ``True`` if *filename* is a valid ZIP file based on its magic number,

Doc/whatsnew/3.14.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,14 @@ uuid
661661
in :rfc:`9562`.
662662
(Contributed by Bénédikt Tran in :gh:`89083`.)
663663

664+
zipinfo
665+
-------
666+
667+
* Added :func:`ZipInfo._for_archive <zipfile.ZipInfo._for_archive>`
668+
to resolve suitable defaults for a :class:`~zipfile.ZipInfo` object
669+
as used by :func:`ZipFile.writestr <zipfile.ZipFile.writestr>`.
670+
671+
(Contributed by Bénédikt Tran in :gh:`123424`.)
664672

665673
.. Add improved modules above alphabetically, not here at the end.
666674

Lib/test/test_zipfile/_path/test_path.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ def test_backslash_not_separator(self):
634634
"""
635635
data = io.BytesIO()
636636
zf = zipfile.ZipFile(data, "w")
637-
zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
637+
zf.writestr(DirtyZipInfo("foo\\bar")._for_archive(zf), b"content")
638638
zf.filename = ''
639639
root = zipfile.Path(zf)
640640
(first,) = root.iterdir()
@@ -657,20 +657,3 @@ class DirtyZipInfo(zipfile.ZipInfo):
657657
def __init__(self, filename, *args, **kwargs):
658658
super().__init__(filename, *args, **kwargs)
659659
self.filename = filename
660-
661-
@classmethod
662-
def for_name(cls, name, archive):
663-
"""
664-
Construct the same way that ZipFile.writestr does.
665-
666-
TODO: extract this functionality and re-use
667-
"""
668-
self = cls(filename=name, date_time=time.localtime(time.time())[:6])
669-
self.compress_type = archive.compression
670-
self.compress_level = archive.compresslevel
671-
if self.filename.endswith('/'): # pragma: no cover
672-
self.external_attr = 0o40775 << 16 # drwxrwxr-x
673-
self.external_attr |= 0x10 # MS-DOS directory flag
674-
else:
675-
self.external_attr = 0o600 << 16 # ?rw-------
676-
return self

Lib/test/test_zipfile/test_core.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import itertools
66
import os
77
import posixpath
8+
import stat
89
import struct
910
import subprocess
1011
import sys
@@ -2211,6 +2212,34 @@ def test_create_empty_zipinfo_repr(self):
22112212
zi = zipfile.ZipInfo(filename="empty")
22122213
self.assertEqual(repr(zi), "<ZipInfo filename='empty' file_size=0>")
22132214

2215+
def test_for_archive(self):
2216+
base_filename = TESTFN2.rstrip('/')
2217+
2218+
with zipfile.ZipFile(TESTFN, mode="w", compresslevel=1,
2219+
compression=zipfile.ZIP_STORED) as zf:
2220+
# no trailing forward slash
2221+
zi = zipfile.ZipInfo(base_filename)._for_archive(zf)
2222+
self.assertEqual(zi.compress_level, 1)
2223+
self.assertEqual(zi.compress_type, zipfile.ZIP_STORED)
2224+
# ?rw- --- ---
2225+
filemode = stat.S_IRUSR | stat.S_IWUSR
2226+
# filemode is stored as the highest 16 bits of external_attr
2227+
self.assertEqual(zi.external_attr >> 16, filemode)
2228+
self.assertEqual(zi.external_attr & 0xFF, 0) # no MS-DOS flag
2229+
2230+
with zipfile.ZipFile(TESTFN, mode="w", compresslevel=1,
2231+
compression=zipfile.ZIP_STORED) as zf:
2232+
# with a trailing slash
2233+
zi = zipfile.ZipInfo(f'{base_filename}/')._for_archive(zf)
2234+
self.assertEqual(zi.compress_level, 1)
2235+
self.assertEqual(zi.compress_type, zipfile.ZIP_STORED)
2236+
# d rwx rwx r-x
2237+
filemode = stat.S_IFDIR
2238+
filemode |= stat.S_IRWXU | stat.S_IRWXG
2239+
filemode |= stat.S_IROTH | stat.S_IXOTH
2240+
self.assertEqual(zi.external_attr >> 16, filemode)
2241+
self.assertEqual(zi.external_attr & 0xFF, 0x10) # MS-DOS flag
2242+
22142243
def test_create_empty_zipinfo_default_attributes(self):
22152244
"""Ensure all required attributes are set."""
22162245
zi = zipfile.ZipInfo()

Lib/zipfile/__init__.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import sys
1414
import threading
1515
import time
16+
from typing import Self
1617

1718
try:
1819
import zlib # We may need its compression method
@@ -605,6 +606,24 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
605606

606607
return zinfo
607608

609+
def _for_archive(self, archive: ZipFile) -> Self:
610+
"""Resolve suitable defaults from the archive.
611+
612+
Resolve the date_time, compression attributes, and external attributes
613+
to suitable defaults as used by :method:`ZipFile.writestr`.
614+
615+
Return self.
616+
"""
617+
self.date_time = time.localtime(time.time())[:6]
618+
self.compress_type = archive.compression
619+
self.compress_level = archive.compresslevel
620+
if self.filename.endswith('/'): # pragma: no cover
621+
self.external_attr = 0o40775 << 16 # drwxrwxr-x
622+
self.external_attr |= 0x10 # MS-DOS directory flag
623+
else:
624+
self.external_attr = 0o600 << 16 # ?rw-------
625+
return self
626+
608627
def is_dir(self):
609628
"""Return True if this archive member is a directory."""
610629
if self.filename.endswith('/'):
@@ -1908,18 +1927,10 @@ def writestr(self, zinfo_or_arcname, data,
19081927
the name of the file in the archive."""
19091928
if isinstance(data, str):
19101929
data = data.encode("utf-8")
1911-
if not isinstance(zinfo_or_arcname, ZipInfo):
1912-
zinfo = ZipInfo(filename=zinfo_or_arcname,
1913-
date_time=time.localtime(time.time())[:6])
1914-
zinfo.compress_type = self.compression
1915-
zinfo.compress_level = self.compresslevel
1916-
if zinfo.filename.endswith('/'):
1917-
zinfo.external_attr = 0o40775 << 16 # drwxrwxr-x
1918-
zinfo.external_attr |= 0x10 # MS-DOS directory flag
1919-
else:
1920-
zinfo.external_attr = 0o600 << 16 # ?rw-------
1921-
else:
1930+
if isinstance(zinfo_or_arcname, ZipInfo):
19221931
zinfo = zinfo_or_arcname
1932+
else:
1933+
zinfo = ZipInfo(zinfo_or_arcname)._for_archive(self)
19231934

19241935
if not self.fp:
19251936
raise ValueError(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)