gh-123424: add ZipInfo._for_archive to set suitable default properties (#123429)

---------

Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
This commit is contained in:
Bénédikt Tran 2024-12-29 19:30:53 +01:00 committed by GitHub
parent ffece5590e
commit 7e819ce0f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 72 additions and 29 deletions

View file

@ -84,6 +84,17 @@ The module defines the following items:
formerly protected :attr:`!_compresslevel`. The older protected name formerly protected :attr:`!_compresslevel`. The older protected name
continues to work as a property for backwards compatibility. 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) .. function:: is_zipfile(filename)
Returns ``True`` if *filename* is a valid ZIP file based on its magic number, Returns ``True`` if *filename* is a valid ZIP file based on its magic number,

View file

@ -661,6 +661,14 @@ uuid
in :rfc:`9562`. in :rfc:`9562`.
(Contributed by Bénédikt Tran in :gh:`89083`.) (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. .. Add improved modules above alphabetically, not here at the end.

View file

@ -634,7 +634,7 @@ class TestPath(unittest.TestCase):
""" """
data = io.BytesIO() data = io.BytesIO()
zf = zipfile.ZipFile(data, "w") 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 = '' zf.filename = ''
root = zipfile.Path(zf) root = zipfile.Path(zf)
(first,) = root.iterdir() (first,) = root.iterdir()
@ -657,20 +657,3 @@ class DirtyZipInfo(zipfile.ZipInfo):
def __init__(self, filename, *args, **kwargs): def __init__(self, filename, *args, **kwargs):
super().__init__(filename, *args, **kwargs) super().__init__(filename, *args, **kwargs)
self.filename = filename 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

View file

@ -5,6 +5,7 @@ import io
import itertools import itertools
import os import os
import posixpath import posixpath
import stat
import struct import struct
import subprocess import subprocess
import sys import sys
@ -2211,6 +2212,34 @@ class OtherTests(unittest.TestCase):
zi = zipfile.ZipInfo(filename="empty") zi = zipfile.ZipInfo(filename="empty")
self.assertEqual(repr(zi), "<ZipInfo filename='empty' file_size=0>") 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): def test_create_empty_zipinfo_default_attributes(self):
"""Ensure all required attributes are set.""" """Ensure all required attributes are set."""
zi = zipfile.ZipInfo() zi = zipfile.ZipInfo()

View file

@ -13,6 +13,7 @@ import struct
import sys import sys
import threading import threading
import time import time
from typing import Self
try: try:
import zlib # We may need its compression method import zlib # We may need its compression method
@ -605,6 +606,24 @@ class ZipInfo:
return zinfo 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): def is_dir(self):
"""Return True if this archive member is a directory.""" """Return True if this archive member is a directory."""
if self.filename.endswith('/'): if self.filename.endswith('/'):
@ -1908,18 +1927,10 @@ class ZipFile:
the name of the file in the archive.""" the name of the file in the archive."""
if isinstance(data, str): if isinstance(data, str):
data = data.encode("utf-8") data = data.encode("utf-8")
if not isinstance(zinfo_or_arcname, ZipInfo): if 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:
zinfo = zinfo_or_arcname zinfo = zinfo_or_arcname
else:
zinfo = ZipInfo(zinfo_or_arcname)._for_archive(self)
if not self.fp: if not self.fp:
raise ValueError( raise ValueError(

View file

@ -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.