Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ The module defines the following items:
formerly protected :attr:`!_compresslevel`. The older protected name
continues to work as a property for backwards compatibility.


.. classmethod:: for_name(filename, archive, *, date_time=None)

Construct an appropriate :class:`ZipInfo` from a *filename*,
a :class:`ZipFile` archive and an optional *date_time*.

If *date_time* is not specified, the current local time is used
instead, namely ``date_time = time.localtime(time.time())[:6]``.

.. versionadded:: 3.14


.. function:: is_zipfile(filename)

Returns ``True`` if *filename* is a valid ZIP file based on its magic number,
Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ symtable

(Contributed by Bénédikt Tran in :gh:`120029`.)


zipinfo
-------

* Added :func:`ZipInfo.for_name <zipfile.ZipInfo.for_name>` for
constructing a :class:`~zipfile.ZipInfo` object from a filename
and an archive.

(Contributed by Bénédikt Tran in :gh:`123424`.)

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

Optimizations
Expand Down
17 changes: 0 additions & 17 deletions Lib/test/test_zipfile/_path/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 29 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import itertools
import os
import posixpath
import stat
import struct
import subprocess
import sys
Expand Down Expand Up @@ -2205,6 +2206,34 @@ def test_create_empty_zipinfo_repr(self):
zi = zipfile.ZipInfo(filename="empty")
self.assertEqual(repr(zi), "<ZipInfo filename='empty' file_size=0>")

def test_create_zipinfo_for_name(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.for_name(base_filename, 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.for_name(f'{base_filename}/', 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()
Expand Down
34 changes: 23 additions & 11 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,26 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):

return zinfo

@classmethod
def for_name(cls, filename, archive, *, date_time=None):
"""Construct an appropriate ZipInfo from a filename and a ZipFile.

The *filename* is expected to be the name of a file in the archive.

If *date_time* is not specified, the current local time is used.
"""
if date_time is None:
date_time = time.localtime(time.time())[:6]
self = cls(filename=filename, date_time=date_time)
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('/'):
Expand Down Expand Up @@ -1903,18 +1923,10 @@ def writestr(self, zinfo_or_arcname, data,
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.for_name(zinfo_or_arcname, self)

if not self.fp:
raise ValueError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :meth:`zipfile.ZipInfo.for_name` factory for constructing
:class:`~zipfile.ZipInfo` objects from a filename and an archive. Patch by
Bénédikt Tran.
Loading