Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,13 @@ Instances have the following methods and attributes:
Size of the uncompressed file.


:class:`ZipInfo` objects support :func:`copy.replace`,
which returns a copy of the object with the specified attributes replaced.

.. versionchanged:: next
Added support for :func:`copy.replace`.


.. _zipfile-commandline:
.. program:: zipfile

Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _pyio
import array
import contextlib
import copy
import errno
import importlib.util
import io
Expand Down Expand Up @@ -5566,6 +5567,42 @@ def test_compresslevel_property(self):
self.assertEqual(zinfo.compress_level, 8)
self.assertEqual(zinfo._compresslevel, 8)

def test_replace(self):
zinfo = zipfile.ZipInfo('name.txt', (2020, 1, 2, 3, 4, 5))
zinfo.file_size = 100
zinfo.CRC = 42
new = copy.replace(zinfo, filename='other.txt',
compress_type=zipfile.ZIP_DEFLATED)
self.assertIsInstance(new, zipfile.ZipInfo)
self.assertEqual(new.filename, 'other.txt')
self.assertEqual(new.orig_filename, 'other.txt')
self.assertEqual(new.compress_type, zipfile.ZIP_DEFLATED)
# Not replaced attributes are inherited from the original object.
self.assertEqual(new.date_time, (2020, 1, 2, 3, 4, 5))
self.assertEqual(new.file_size, 100)
self.assertEqual(new.CRC, 42)
# The original object is left unchanged.
self.assertEqual(zinfo.filename, 'name.txt')
self.assertEqual(zinfo.compress_type, zipfile.ZIP_STORED)

new = copy.replace(zinfo, date_time=(1980, 1, 1, 0, 0, 0))
self.assertEqual(new.date_time, (1980, 1, 1, 0, 0, 0))
with self.assertRaises(ValueError):
copy.replace(zinfo, date_time=(1979, 12, 31, 0, 0, 0))
with self.assertRaisesRegex(TypeError, "'filenam'"):
copy.replace(zinfo, filenam='other.txt')
with self.assertRaisesRegex(TypeError, "'orig_filename'"):
copy.replace(zinfo, orig_filename='other.txt')

def test_replace_unset_attributes(self):
# header_offset and CRC are only set when the object is written
# to or read from an archive.
zinfo = zipfile.ZipInfo('name.txt')
new = copy.replace(zinfo, file_size=5)
self.assertEqual(new.file_size, 5)
self.assertFalse(hasattr(new, 'header_offset'))
self.assertFalse(hasattr(new, 'CRC'))


class CommandLineTest(unittest.TestCase):

Expand Down
22 changes: 22 additions & 0 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
XXX references to utf-8 need further investigation.
"""
import binascii
import copy
import io
import os
import shutil
Expand Down Expand Up @@ -501,6 +502,22 @@ def _compresslevel(self):
def _compresslevel(self, value):
self.compress_level = value

def __replace__(self, /, **changes):
unexpected = changes.keys() - _zipinfo_attributes
if unexpected:
raise TypeError(f'__replace__() got an unexpected keyword '
f'argument {min(unexpected)!r}')
new = copy.copy(self)
if 'filename' in changes:
filename = changes.pop('filename')
new.orig_filename = filename
new.filename = _sanitize_filename(filename)
if changes.get('date_time', (1980,))[0] < 1980:
raise ValueError('ZIP does not support timestamps before 1980')
for name, value in changes.items():
setattr(new, name, value)
return new

def __repr__(self):
result = ['<%s filename=%r' % (self.__class__.__name__, self.filename)]
if self.compress_type != ZIP_STORED:
Expand Down Expand Up @@ -705,6 +722,11 @@ def is_dir(self):
return False


# Attributes which can be replaced by ZipInfo.__replace__().
_zipinfo_attributes = frozenset(ZipInfo.__slots__) - {
'orig_filename', '_raw_time', '_end_offset'}


# ZIP encryption uses the CRC32 one-byte primitive for scrambling some
# internal keys. We noticed that a direct implementation is faster than
# relying on binascii.crc32().
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:class:`zipfile.ZipInfo` objects now support :func:`copy.replace`.
Loading