zipremove 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipremove
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Extend `zipfile` with `remove`-related functionalities
5
5
  Home-page: https://github.com/danny0838/zipremove
6
6
  Author: Danny Lin
@@ -52,7 +52,7 @@ This package extends `zipfile` with `remove`-related functionalities.
52
52
  a path is provided.
53
53
 
54
54
  This does not physically remove the local file entry from the archive.
55
- Call `ZipFile.repack` afterwards to reclaim space.
55
+ Call `repack` afterwards to reclaim space.
56
56
 
57
57
  The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``.
58
58
 
@@ -18,7 +18,7 @@ This package extends `zipfile` with `remove`-related functionalities.
18
18
  a path is provided.
19
19
 
20
20
  This does not physically remove the local file entry from the archive.
21
- Call `ZipFile.repack` afterwards to reclaim space.
21
+ Call `repack` afterwards to reclaim space.
22
22
 
23
23
  The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``.
24
24
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = zipremove
3
- version = 0.4.0
3
+ version = 0.5.0
4
4
  author = Danny Lin
5
5
  author_email = danny0838@gmail.com
6
6
  url = https://github.com/danny0838/zipremove
@@ -82,7 +82,7 @@ class _ZipRepacker:
82
82
 
83
83
  def copy(self, zfile, zinfo, filename):
84
84
  # make a copy of zinfo
85
- zinfo2 = copy.deepcopy(zinfo)
85
+ zinfo2 = copy.copy(zinfo)
86
86
 
87
87
  # apply sanitized new filename as in `ZipInfo.__init__`
88
88
  zinfo2.orig_filename = filename
@@ -90,7 +90,7 @@ class _ZipRepacker:
90
90
 
91
91
  zinfo2.header_offset = zfile.start_dir
92
92
 
93
- # polyfill: update zinfo2._end_offset if exists
93
+ # polyfill: clear zinfo2._end_offset if exists
94
94
  # (Python >= 3.8 with fix #109858)
95
95
  if hasattr(zinfo2, '_end_offset'):
96
96
  zinfo2._end_offset = None
@@ -113,10 +113,9 @@ class _ZipRepacker:
113
113
  """
114
114
  Repack the ZIP file, stripping unreferenced local file entries.
115
115
 
116
- Assumes that local file entries are stored consecutively, with no gaps
117
- or overlaps.
118
-
119
- Behavior:
116
+ Assumes that local file entries (and the central directory, which is
117
+ mostly treated as the "last entry") are stored consecutively, with no
118
+ gaps or overlaps:
120
119
 
121
120
  1. If any referenced entry overlaps with another, a `BadZipFile` error
122
121
  is raised since safe repacking cannot be guaranteed.
@@ -129,8 +128,8 @@ class _ZipRepacker:
129
128
  be a sequence of consecutive entries with no extra preceding bytes;
130
129
  extra following bytes are preserved.
131
130
 
132
- 4. This is to prevent an unexpected data removal (false positive),
133
- though a false negative may happen in certain rare cases.
131
+ This is to prevent an unexpected data removal (false positive), though
132
+ a false negative may happen in certain rare cases.
134
133
 
135
134
  Examples:
136
135
 
@@ -180,8 +179,8 @@ class _ZipRepacker:
180
179
  - Modifies the ZIP file in place.
181
180
  - Updates zfile.start_dir to account for removed data.
182
181
  - Sets zfile._didModify to True.
183
- - Updates header_offset and _end_offset of referenced ZipInfo
184
- instances.
182
+ - Updates header_offset and clears _end_offset of referenced
183
+ ZipInfo instances.
185
184
 
186
185
  Parameters:
187
186
  zfile: A ZipFile object representing the archive to repack.
@@ -262,14 +261,11 @@ class _ZipRepacker:
262
261
  used_entry_size,
263
262
  )
264
263
 
265
- if used_entry_size < entry_size:
266
- stale_entry_size = self._validate_local_file_entry_sequence(
267
- fp,
268
- old_header_offset + used_entry_size,
269
- old_header_offset + entry_size,
270
- )
271
- else:
272
- stale_entry_size = 0
264
+ stale_entry_size = self._validate_local_file_entry_sequence(
265
+ fp,
266
+ old_header_offset + used_entry_size,
267
+ old_header_offset + entry_size,
268
+ )
273
269
 
274
270
  if stale_entry_size > 0:
275
271
  self._copy_bytes(
@@ -286,17 +282,11 @@ class _ZipRepacker:
286
282
  zfile.start_dir -= entry_offset
287
283
  zfile._didModify = True
288
284
 
289
- # polyfill: update ZipInfo._end_offset if exists
285
+ # polyfill: clear ZipInfo._end_offset if exists
290
286
  # (Python >= 3.8 with fix #109858)
291
287
  if hasattr(ZipInfo, '_end_offset'):
292
- end_offset = zfile.start_dir
293
- for zinfo in reversed(filelist):
294
- if zinfo in removed_zinfos:
295
- zinfo._end_offset = None
296
- else:
297
- if zinfo._end_offset is not None:
298
- zinfo._end_offset = end_offset
299
- end_offset = zinfo.header_offset
288
+ for zinfo in filelist:
289
+ zinfo._end_offset = None
300
290
 
301
291
  def _calc_initial_entry_offset(self, fp, data_offset):
302
292
  checked_offsets = {}
@@ -380,8 +370,8 @@ class _ZipRepacker:
380
370
  if pos > end_offset:
381
371
  return None
382
372
 
373
+ # parse zip64
383
374
  try:
384
- # parse zip64
385
375
  try:
386
376
  zinfo._decodeExtra(crc32(filename))
387
377
  except TypeError:
@@ -617,15 +607,16 @@ class ZipFile(ZipFile):
617
607
 
618
608
  with self._lock:
619
609
  # get the zinfo
620
- # raise KeyError if arcname does not exist
621
610
  if isinstance(zinfo_or_arcname, ZipInfo):
622
611
  zinfo = zinfo_or_arcname
623
- if zinfo not in self.filelist:
624
- raise KeyError('There is no item %r in the archive' % zinfo)
625
612
  else:
613
+ # raise KeyError if arcname does not exist
626
614
  zinfo = self.getinfo(zinfo_or_arcname)
627
615
 
628
- self.filelist.remove(zinfo)
616
+ try:
617
+ self.filelist.remove(zinfo)
618
+ except ValueError:
619
+ raise KeyError('There is no item %r in the archive' % zinfo) from None
629
620
 
630
621
  try:
631
622
  del self.NameToInfo[zinfo.filename]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipremove
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Extend `zipfile` with `remove`-related functionalities
5
5
  Home-page: https://github.com/danny0838/zipremove
6
6
  Author: Danny Lin
@@ -52,7 +52,7 @@ This package extends `zipfile` with `remove`-related functionalities.
52
52
  a path is provided.
53
53
 
54
54
  This does not physically remove the local file entry from the archive.
55
- Call `ZipFile.repack` afterwards to reclaim space.
55
+ Call `repack` afterwards to reclaim space.
56
56
 
57
57
  The archive must be opened with mode ``'w'``, ``'x'`` or ``'a'``.
58
58
 
@@ -1,12 +1,13 @@
1
+ import contextlib
1
2
  import io
2
3
  import itertools
3
4
  import os
4
5
  import struct
5
6
  import sys
7
+ import time
6
8
  import unittest
7
9
  import unittest.mock as mock
8
10
  import warnings
9
- from contextlib import nullcontext
10
11
 
11
12
  import zipremove as zipfile
12
13
 
@@ -42,8 +43,11 @@ def requires_zip64fix(reason='requires Python >= 3.11.4 for zip64 fix (#103861)'
42
43
  return unittest.skipUnless(sys.version_info >= (3, 11, 4), reason)
43
44
 
44
45
 
45
- def ComparableZipInfo(zinfo):
46
- return (zinfo.filename, zinfo.header_offset, zinfo.compress_size, zinfo.CRC)
46
+ class ComparableZipInfo:
47
+ keys = [i for i in zipfile.ZipInfo.__slots__ if not i.startswith('_')]
48
+
49
+ def __new__(cls, zinfo):
50
+ return {i: getattr(zinfo, i) for i in cls.keys}
47
51
 
48
52
  _struct_pack = struct.pack
49
53
 
@@ -59,6 +63,8 @@ def struct_pack_no_dd_sig(fmt, *values):
59
63
 
60
64
  class RepackHelperMixin:
61
65
  """Common helpers for remove and repack."""
66
+ maxDiff = 8192
67
+
62
68
  @classmethod
63
69
  def _prepare_test_files(cls):
64
70
  return [
@@ -69,37 +75,48 @@ class RepackHelperMixin:
69
75
 
70
76
  @classmethod
71
77
  def _prepare_zip_from_test_files(cls, zfname, test_files, force_zip64=False):
72
- zinfos = []
73
78
  with zipfile.ZipFile(zfname, 'w', cls.compression) as zh:
74
79
  for file, data in test_files:
75
80
  with zh.open(file, 'w', force_zip64=force_zip64) as fh:
76
81
  fh.write(data)
77
- zinfo = zh.getinfo(file)
78
- zinfos.append(ComparableZipInfo(zinfo))
79
- return zinfos
82
+ return list(zh.infolist())
80
83
 
81
84
  class AbstractCopyTests(RepackHelperMixin):
82
85
  @classmethod
83
86
  def setUpClass(cls):
84
87
  cls.test_files = cls._prepare_test_files()
85
88
 
89
+ def tearDown(self):
90
+ unlink(TESTFN)
91
+
86
92
  def test_copy_by_name(self):
87
93
  for i in range(3):
88
94
  with self.subTest(i=i, filename=self.test_files[i][0]):
89
95
  zinfos = self._prepare_zip_from_test_files(TESTFN, self.test_files)
90
96
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
91
- zi_new = ('file.txt', zh.start_dir, *zinfos[i][2:])
97
+ zi_new = {
98
+ **ComparableZipInfo(zinfos[i]),
99
+ 'filename': 'file.txt',
100
+ 'orig_filename': 'file.txt',
101
+ 'header_offset': zh.start_dir,
102
+ }
92
103
  zh.copy(self.test_files[i][0], 'file.txt')
93
104
 
94
105
  # check infolist
95
106
  self.assertEqual(
96
107
  [ComparableZipInfo(zi) for zi in zh.infolist()],
97
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
108
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
98
109
  )
99
110
 
100
111
  # check NameToInfo cache
101
112
  self.assertEqual(ComparableZipInfo(zh.getinfo('file.txt')), zi_new)
102
113
 
114
+ # check content
115
+ self.assertEqual(
116
+ zh.read(zi_new['filename']),
117
+ zh.read(zinfos[i].filename),
118
+ )
119
+
103
120
  # make sure the zip file is still valid
104
121
  with zipfile.ZipFile(TESTFN) as zh:
105
122
  self.assertIsNone(zh.testzip())
@@ -109,18 +126,29 @@ class AbstractCopyTests(RepackHelperMixin):
109
126
  with self.subTest(i=i, filename=self.test_files[i][0]):
110
127
  zinfos = self._prepare_zip_from_test_files(TESTFN, self.test_files)
111
128
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
112
- zi_new = ('file.txt', zh.start_dir, *zinfos[i][2:])
129
+ zi_new = {
130
+ **ComparableZipInfo(zinfos[i]),
131
+ 'filename': 'file.txt',
132
+ 'orig_filename': 'file.txt',
133
+ 'header_offset': zh.start_dir,
134
+ }
113
135
  zh.copy(zh.infolist()[i], 'file.txt')
114
136
 
115
137
  # check infolist
116
138
  self.assertEqual(
117
139
  [ComparableZipInfo(zi) for zi in zh.infolist()],
118
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
140
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
119
141
  )
120
142
 
121
143
  # check NameToInfo cache
122
144
  self.assertEqual(ComparableZipInfo(zh.getinfo('file.txt')), zi_new)
123
145
 
146
+ # check content
147
+ self.assertEqual(
148
+ zh.read(zi_new['filename']),
149
+ zh.read(zinfos[i].filename),
150
+ )
151
+
124
152
  # make sure the zip file is still valid
125
153
  with zipfile.ZipFile(TESTFN) as zh:
126
154
  self.assertIsNone(zh.testzip())
@@ -130,18 +158,29 @@ class AbstractCopyTests(RepackHelperMixin):
130
158
  with self.subTest(i=i, filename=self.test_files[i][0]):
131
159
  zinfos = self._prepare_zip_from_test_files(TESTFN, self.test_files, force_zip64=True)
132
160
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
133
- zi_new = ('file.txt', zh.start_dir, *zinfos[i][2:])
161
+ zi_new = {
162
+ **ComparableZipInfo(zinfos[i]),
163
+ 'filename': 'file.txt',
164
+ 'orig_filename': 'file.txt',
165
+ 'header_offset': zh.start_dir,
166
+ }
134
167
  zh.copy(self.test_files[i][0], 'file.txt')
135
168
 
136
169
  # check infolist
137
170
  self.assertEqual(
138
171
  [ComparableZipInfo(zi) for zi in zh.infolist()],
139
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
172
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
140
173
  )
141
174
 
142
175
  # check NameToInfo cache
143
176
  self.assertEqual(ComparableZipInfo(zh.getinfo('file.txt')), zi_new)
144
177
 
178
+ # check content
179
+ self.assertEqual(
180
+ zh.read(zi_new['filename']),
181
+ zh.read(zinfos[i].filename),
182
+ )
183
+
145
184
  # make sure the zip file is still valid
146
185
  with zipfile.ZipFile(TESTFN) as zh:
147
186
  self.assertIsNone(zh.testzip())
@@ -152,18 +191,29 @@ class AbstractCopyTests(RepackHelperMixin):
152
191
  with open(TESTFN, 'wb') as fh:
153
192
  zinfos = self._prepare_zip_from_test_files(Unseekable(fh), self.test_files)
154
193
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
155
- zi_new = ('file.txt', zh.start_dir, *zinfos[i][2:])
194
+ zi_new = {
195
+ **ComparableZipInfo(zinfos[i]),
196
+ 'filename': 'file.txt',
197
+ 'orig_filename': 'file.txt',
198
+ 'header_offset': zh.start_dir,
199
+ }
156
200
  zh.copy(self.test_files[i][0], 'file.txt')
157
201
 
158
202
  # check infolist
159
203
  self.assertEqual(
160
204
  [ComparableZipInfo(zi) for zi in zh.infolist()],
161
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
205
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
162
206
  )
163
207
 
164
208
  # check NameToInfo cache
165
209
  self.assertEqual(ComparableZipInfo(zh.getinfo('file.txt')), zi_new)
166
210
 
211
+ # check content
212
+ self.assertEqual(
213
+ zh.read(zi_new['filename']),
214
+ zh.read(zinfos[i].filename),
215
+ )
216
+
167
217
  # make sure the zip file is still valid
168
218
  with zipfile.ZipFile(TESTFN) as zh:
169
219
  self.assertIsNone(zh.testzip())
@@ -173,18 +223,29 @@ class AbstractCopyTests(RepackHelperMixin):
173
223
  with self.subTest(i=i, filename=self.test_files[i][0]):
174
224
  zinfos = self._prepare_zip_from_test_files(TESTFN, self.test_files)
175
225
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
176
- zi_new = ('file2.txt', zh.start_dir, *zinfos[i][2:])
226
+ zi_new = {
227
+ **ComparableZipInfo(zinfos[i]),
228
+ 'filename': 'file2.txt',
229
+ 'orig_filename': 'file2.txt',
230
+ 'header_offset': zh.start_dir,
231
+ }
177
232
  zh.copy(self.test_files[i][0], 'file2.txt')
178
233
 
179
234
  # check infolist
180
235
  self.assertEqual(
181
236
  [ComparableZipInfo(zi) for zi in zh.infolist()],
182
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
237
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
183
238
  )
184
239
 
185
240
  # check NameToInfo cache
186
241
  self.assertEqual(ComparableZipInfo(zh.getinfo('file2.txt')), zi_new)
187
242
 
243
+ # check content
244
+ self.assertEqual(
245
+ zh.read(zi_new['filename']),
246
+ zh.read(zinfos[i].filename),
247
+ )
248
+
188
249
  # make sure the zip file is still valid
189
250
  with zipfile.ZipFile(TESTFN) as zh:
190
251
  self.assertIsNone(zh.testzip())
@@ -222,44 +283,63 @@ class AbstractCopyTests(RepackHelperMixin):
222
283
  with zipfile.ZipFile(TESTFN, 'w') as zh:
223
284
  for file, data in self.test_files:
224
285
  zh.writestr(file, data)
225
- zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
226
-
227
- zi_new = ('file.txt', zh.start_dir, *zinfos[0][2:])
286
+ zinfos = list(zh.infolist())
287
+
288
+ zi_new = {
289
+ **ComparableZipInfo(zinfos[0]),
290
+ 'filename': 'file.txt',
291
+ 'orig_filename': 'file.txt',
292
+ 'header_offset': zh.start_dir,
293
+ }
228
294
  zh.copy(zh.infolist()[0], 'file.txt')
229
295
 
230
296
  # check infolist
231
297
  self.assertEqual(
232
298
  [ComparableZipInfo(zi) for zi in zh.infolist()],
233
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
299
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
234
300
  )
235
301
 
236
302
  # check NameToInfo cache
237
303
  self.assertEqual(ComparableZipInfo(zh.getinfo('file.txt')), zi_new)
238
- zh.remove(self.test_files[0][0])
304
+
305
+ # check content
306
+ self.assertEqual(
307
+ zh.read(zi_new['filename']),
308
+ zh.read(zinfos[0].filename),
309
+ )
239
310
 
240
311
  # make sure the zip file is still valid
241
312
  with zipfile.ZipFile(TESTFN) as zh:
242
313
  self.assertIsNone(zh.testzip())
243
314
 
244
315
  def test_copy_mode_x(self):
245
- unlink(TESTFN)
246
316
  with zipfile.ZipFile(TESTFN, 'x') as zh:
247
317
  for file, data in self.test_files:
248
318
  zh.writestr(file, data)
249
- zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
250
-
251
- zi_new = ('file.txt', zh.start_dir, *zinfos[0][2:])
319
+ zinfos = list(zh.infolist())
320
+
321
+ zi_new = {
322
+ **ComparableZipInfo(zinfos[0]),
323
+ 'filename': 'file.txt',
324
+ 'orig_filename': 'file.txt',
325
+ 'header_offset': zh.start_dir,
326
+ }
252
327
  zh.copy(zh.infolist()[0], 'file.txt')
253
328
 
254
329
  # check infolist
255
330
  self.assertEqual(
256
331
  [ComparableZipInfo(zi) for zi in zh.infolist()],
257
- [*(zi for j, zi in enumerate(zinfos)), zi_new],
332
+ [*(ComparableZipInfo(zi) for zi in zinfos), zi_new],
258
333
  )
259
334
 
260
335
  # check NameToInfo cache
261
336
  self.assertEqual(ComparableZipInfo(zh.getinfo('file.txt')), zi_new)
262
- zh.remove(self.test_files[0][0])
337
+
338
+ # check content
339
+ self.assertEqual(
340
+ zh.read(zi_new['filename']),
341
+ zh.read(zinfos[0].filename),
342
+ )
263
343
 
264
344
  # make sure the zip file is still valid
265
345
  with zipfile.ZipFile(TESTFN) as zh:
@@ -302,7 +382,7 @@ class AbstractRemoveTests(RepackHelperMixin):
302
382
  # check infolist
303
383
  self.assertEqual(
304
384
  [ComparableZipInfo(zi) for zi in zh.infolist()],
305
- [zi for j, zi in enumerate(zinfos) if j != i],
385
+ [ComparableZipInfo(zi) for j, zi in enumerate(zinfos) if j != i],
306
386
  )
307
387
 
308
388
  # check NameToInfo cache
@@ -323,7 +403,7 @@ class AbstractRemoveTests(RepackHelperMixin):
323
403
  # check infolist
324
404
  self.assertEqual(
325
405
  [ComparableZipInfo(zi) for zi in zh.infolist()],
326
- [zi for j, zi in enumerate(zinfos) if j != i],
406
+ [ComparableZipInfo(zi) for j, zi in enumerate(zinfos) if j != i],
327
407
  )
328
408
 
329
409
  # check NameToInfo cache
@@ -364,13 +444,13 @@ class AbstractRemoveTests(RepackHelperMixin):
364
444
  # check infolist
365
445
  self.assertEqual(
366
446
  [ComparableZipInfo(zi) for zi in zh.infolist()],
367
- [zinfos[0], zinfos[2]],
447
+ [ComparableZipInfo(zi) for zi in [zinfos[0], zinfos[2]]],
368
448
  )
369
449
 
370
450
  # check NameToInfo cache
371
451
  self.assertEqual(
372
452
  ComparableZipInfo(zh.getinfo('file.txt')),
373
- zinfos[0],
453
+ ComparableZipInfo(zinfos[0]),
374
454
  )
375
455
 
376
456
  # make sure the zip file is still valid
@@ -385,7 +465,7 @@ class AbstractRemoveTests(RepackHelperMixin):
385
465
  # check infolist
386
466
  self.assertEqual(
387
467
  [ComparableZipInfo(zi) for zi in zh.infolist()],
388
- [zinfos[2]],
468
+ [ComparableZipInfo(zi) for zi in [zinfos[2]]],
389
469
  )
390
470
 
391
471
  # check NameToInfo cache
@@ -414,13 +494,13 @@ class AbstractRemoveTests(RepackHelperMixin):
414
494
  # check infolist
415
495
  self.assertEqual(
416
496
  [ComparableZipInfo(zi) for zi in zh.infolist()],
417
- [zinfos[1], zinfos[2]],
497
+ [ComparableZipInfo(zi) for zi in [zinfos[1], zinfos[2]]],
418
498
  )
419
499
 
420
500
  # check NameToInfo cache
421
501
  self.assertEqual(
422
502
  ComparableZipInfo(zh.getinfo('file.txt')),
423
- zinfos[1],
503
+ ComparableZipInfo(zinfos[1]),
424
504
  )
425
505
 
426
506
  # make sure the zip file is still valid
@@ -434,13 +514,13 @@ class AbstractRemoveTests(RepackHelperMixin):
434
514
  # check infolist
435
515
  self.assertEqual(
436
516
  [ComparableZipInfo(zi) for zi in zh.infolist()],
437
- [zinfos[0], zinfos[2]],
517
+ [ComparableZipInfo(zi) for zi in [zinfos[0], zinfos[2]]],
438
518
  )
439
519
 
440
520
  # check NameToInfo cache
441
521
  self.assertEqual(
442
522
  ComparableZipInfo(zh.getinfo('file.txt')),
443
- zinfos[0],
523
+ ComparableZipInfo(zinfos[0]),
444
524
  )
445
525
 
446
526
  # make sure the zip file is still valid
@@ -456,7 +536,7 @@ class AbstractRemoveTests(RepackHelperMixin):
456
536
  # check infolist
457
537
  self.assertEqual(
458
538
  [ComparableZipInfo(zi) for zi in zh.infolist()],
459
- [zinfos[2]],
539
+ [ComparableZipInfo(zi) for zi in [zinfos[2]]],
460
540
  )
461
541
 
462
542
  # check NameToInfo cache
@@ -478,7 +558,7 @@ class AbstractRemoveTests(RepackHelperMixin):
478
558
  # check infolist
479
559
  self.assertEqual(
480
560
  [ComparableZipInfo(zi) for zi in zh.infolist()],
481
- [zi for j, zi in enumerate(zinfos) if j != i],
561
+ [ComparableZipInfo(zi) for j, zi in enumerate(zinfos) if j != i],
482
562
  )
483
563
 
484
564
  # check NameToInfo cache
@@ -513,14 +593,14 @@ class AbstractRemoveTests(RepackHelperMixin):
513
593
  with zipfile.ZipFile(TESTFN, 'w') as zh:
514
594
  for file, data in self.test_files:
515
595
  zh.writestr(file, data)
516
- zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
596
+ zinfos = list(zh.infolist())
517
597
 
518
598
  zh.remove(self.test_files[0][0])
519
599
 
520
600
  # check infolist
521
601
  self.assertEqual(
522
602
  [ComparableZipInfo(zi) for zi in zh.infolist()],
523
- [zinfos[1], zinfos[2]],
603
+ [ComparableZipInfo(zi) for zi in [zinfos[1], zinfos[2]]],
524
604
  )
525
605
 
526
606
  # check NameToInfo cache
@@ -535,14 +615,14 @@ class AbstractRemoveTests(RepackHelperMixin):
535
615
  with zipfile.ZipFile(TESTFN, 'x') as zh:
536
616
  for file, data in self.test_files:
537
617
  zh.writestr(file, data)
538
- zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
618
+ zinfos = list(zh.infolist())
539
619
 
540
620
  zh.remove(self.test_files[0][0])
541
621
 
542
622
  # check infolist
543
623
  self.assertEqual(
544
624
  [ComparableZipInfo(zi) for zi in zh.infolist()],
545
- [zinfos[1], zinfos[2]],
625
+ [ComparableZipInfo(zi) for zi in [zinfos[1], zinfos[2]]],
546
626
  )
547
627
 
548
628
  # check NameToInfo cache
@@ -601,7 +681,7 @@ class AbstractRepackTests(RepackHelperMixin):
601
681
  # check infolist
602
682
  self.assertEqual(
603
683
  [ComparableZipInfo(zi) for zi in zh.infolist()],
604
- expected_zinfos,
684
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
605
685
  )
606
686
 
607
687
  # check file size
@@ -616,27 +696,20 @@ class AbstractRepackTests(RepackHelperMixin):
616
696
  self._prepare_zip_from_test_files(TESTFN, self.test_files)
617
697
 
618
698
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
619
- zi = zh.remove(zh.infolist()[0])
620
- with mock.patch.object(zipfile._ZipRepacker, 'repack') as m_rp:
699
+ with mock.patch.object(zipfile._ZipRepacker, 'repack') as m_rp, \
700
+ mock.patch.object(zipfile, '_ZipRepacker', wraps=zipfile._ZipRepacker) as m_zr:
621
701
  zh.repack()
702
+ m_zr.assert_called_once_with()
622
703
  m_rp.assert_called_once_with(zh, None)
623
704
 
624
705
  with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
625
706
  zi = zh.remove(zh.infolist()[0])
626
- with mock.patch.object(zipfile._ZipRepacker, 'repack') as m_rp:
627
- zh.repack([zi])
707
+ with mock.patch.object(zipfile._ZipRepacker, 'repack') as m_rp, \
708
+ mock.patch.object(zipfile, '_ZipRepacker', wraps=zipfile._ZipRepacker) as m_zr:
709
+ zh.repack([zi], strict_descriptor=True, chunk_size=1024)
710
+ m_zr.assert_called_once_with(strict_descriptor=True, chunk_size=1024)
628
711
  m_rp.assert_called_once_with(zh, [zi])
629
712
 
630
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
631
- with mock.patch.object(zipfile, '_ZipRepacker') as m_rp:
632
- zh.repack()
633
- m_rp.assert_called_once_with()
634
-
635
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
636
- with mock.patch.object(zipfile, '_ZipRepacker') as m_rp:
637
- zh.repack(strict_descriptor=True, chunk_size=1024)
638
- m_rp.assert_called_once_with(strict_descriptor=True, chunk_size=1024)
639
-
640
713
  def test_repack_bytes_before_first_file(self):
641
714
  """Should preserve random bytes before the first recorded local file entry."""
642
715
  for ii in ([], [0], [0, 1], [0, 1, 2]):
@@ -660,7 +733,7 @@ class AbstractRepackTests(RepackHelperMixin):
660
733
  # check infolist
661
734
  self.assertEqual(
662
735
  [ComparableZipInfo(zi) for zi in zh.infolist()],
663
- expected_zinfos,
736
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
664
737
  )
665
738
 
666
739
  # check file size
@@ -694,7 +767,7 @@ class AbstractRepackTests(RepackHelperMixin):
694
767
  # check infolist
695
768
  self.assertEqual(
696
769
  [ComparableZipInfo(zi) for zi in zh.infolist()],
697
- expected_zinfos,
770
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
698
771
  )
699
772
 
700
773
  # check file size
@@ -740,7 +813,7 @@ class AbstractRepackTests(RepackHelperMixin):
740
813
  # check infolist
741
814
  self.assertEqual(
742
815
  [ComparableZipInfo(zi) for zi in zh.infolist()],
743
- expected_zinfos,
816
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
744
817
  )
745
818
 
746
819
  # check file size
@@ -750,6 +823,7 @@ class AbstractRepackTests(RepackHelperMixin):
750
823
  with zipfile.ZipFile(TESTFN) as zh:
751
824
  self.assertIsNone(zh.testzip())
752
825
 
826
+ @mock.patch.object(time, 'time', new=lambda: 315504000) # fix time for ZipFile.writestr()
753
827
  def test_repack_bytes_before_removed_files(self):
754
828
  """Should preserve if there are bytes before stale local file entries."""
755
829
  for ii in ([1], [1, 2], [2]):
@@ -764,7 +838,7 @@ class AbstractRepackTests(RepackHelperMixin):
764
838
  zh.writestr(file, data)
765
839
  for i in ii:
766
840
  zh.remove(self.test_files[i][0])
767
- expected_zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
841
+ expected_zinfos = list(zh.infolist())
768
842
  expected_size = os.path.getsize(TESTFN)
769
843
 
770
844
  # do the removal and check the result
@@ -783,7 +857,7 @@ class AbstractRepackTests(RepackHelperMixin):
783
857
  # check infolist
784
858
  self.assertEqual(
785
859
  [ComparableZipInfo(zi) for zi in zh.infolist()],
786
- expected_zinfos,
860
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
787
861
  )
788
862
 
789
863
  # check file size
@@ -793,6 +867,7 @@ class AbstractRepackTests(RepackHelperMixin):
793
867
  with zipfile.ZipFile(TESTFN) as zh:
794
868
  self.assertIsNone(zh.testzip())
795
869
 
870
+ @mock.patch.object(time, 'time', new=lambda: 315504000) # fix time for ZipFile.writestr()
796
871
  def test_repack_bytes_after_removed_files(self):
797
872
  """Should keep extra bytes if there are bytes after stale local file entries."""
798
873
  for ii in ([1], [1, 2], [2]):
@@ -806,7 +881,7 @@ class AbstractRepackTests(RepackHelperMixin):
806
881
  if i == ii[-1]:
807
882
  fh.write(b' dummy bytes ')
808
883
  zh.start_dir = fh.tell()
809
- expected_zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
884
+ expected_zinfos = list(zh.infolist())
810
885
  expected_size = os.path.getsize(TESTFN)
811
886
 
812
887
  # do the removal and check the result
@@ -825,7 +900,7 @@ class AbstractRepackTests(RepackHelperMixin):
825
900
  # check infolist
826
901
  self.assertEqual(
827
902
  [ComparableZipInfo(zi) for zi in zh.infolist()],
828
- expected_zinfos,
903
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
829
904
  )
830
905
 
831
906
  # check file size
@@ -835,6 +910,7 @@ class AbstractRepackTests(RepackHelperMixin):
835
910
  with zipfile.ZipFile(TESTFN) as zh:
836
911
  self.assertIsNone(zh.testzip())
837
912
 
913
+ @mock.patch.object(time, 'time', new=lambda: 315504000) # fix time for ZipFile.writestr()
838
914
  def test_repack_bytes_between_removed_files(self):
839
915
  """Should strip only local file entries before random bytes."""
840
916
  # calculate the expected results
@@ -845,7 +921,7 @@ class AbstractRepackTests(RepackHelperMixin):
845
921
  zh.start_dir = fh.tell()
846
922
  zh.writestr(*self.test_files[2])
847
923
  zh.remove(self.test_files[2][0])
848
- expected_zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
924
+ expected_zinfos = list(zh.infolist())
849
925
  expected_size = os.path.getsize(TESTFN)
850
926
 
851
927
  # do the removal and check the result
@@ -864,7 +940,7 @@ class AbstractRepackTests(RepackHelperMixin):
864
940
  # check infolist
865
941
  self.assertEqual(
866
942
  [ComparableZipInfo(zi) for zi in zh.infolist()],
867
- expected_zinfos,
943
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
868
944
  )
869
945
 
870
946
  # check file size
@@ -886,7 +962,7 @@ class AbstractRepackTests(RepackHelperMixin):
886
962
  fh.write(b'dummy ')
887
963
  fh.write(fz.read())
888
964
  with zipfile.ZipFile(TESTFN) as zh:
889
- expected_zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
965
+ expected_zinfos = list(zh.infolist())
890
966
  expected_size = os.path.getsize(TESTFN)
891
967
 
892
968
  # do the removal and check the result
@@ -904,7 +980,7 @@ class AbstractRepackTests(RepackHelperMixin):
904
980
  # check infolist
905
981
  self.assertEqual(
906
982
  [ComparableZipInfo(zi) for zi in zh.infolist()],
907
- expected_zinfos,
983
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
908
984
  )
909
985
 
910
986
  # check file size
@@ -949,7 +1025,7 @@ class AbstractRepackTests(RepackHelperMixin):
949
1025
  # check infolist
950
1026
  self.assertEqual(
951
1027
  [ComparableZipInfo(zi) for zi in zh.infolist()],
952
- expected_zinfos,
1028
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
953
1029
  )
954
1030
 
955
1031
  # check file size
@@ -992,20 +1068,20 @@ class AbstractRepackTests(RepackHelperMixin):
992
1068
  with zipfile.ZipFile(TESTFN) as zh:
993
1069
  self.assertIsNone(zh.testzip())
994
1070
 
1071
+ @mock.patch.object(time, 'time', new=lambda: 315504000) # fix time for ZipFile.writestr()
995
1072
  def test_repack_removed_bytes_between_files(self):
996
1073
  """Should not remove bytes between local file entries."""
997
1074
  for ii in ([0], [1], [2]):
998
1075
  with self.subTest(removed=ii):
999
1076
  # calculate the expected results
1000
- expected_zinfos = []
1001
1077
  with open(TESTFN, 'wb') as fh:
1002
1078
  with zipfile.ZipFile(fh, 'w', self.compression) as zh:
1003
1079
  for j, (file, data) in enumerate(self.test_files):
1004
1080
  if j not in ii:
1005
1081
  zh.writestr(file, data)
1006
- expected_zinfos.append(ComparableZipInfo(zh.getinfo(file)))
1007
1082
  fh.write(b' dummy bytes ')
1008
1083
  zh.start_dir = fh.tell()
1084
+ expected_zinfos = list(zh.infolist())
1009
1085
  expected_size = os.path.getsize(TESTFN)
1010
1086
 
1011
1087
  # do the removal and check the result
@@ -1022,7 +1098,7 @@ class AbstractRepackTests(RepackHelperMixin):
1022
1098
  # check infolist
1023
1099
  self.assertEqual(
1024
1100
  [ComparableZipInfo(zi) for zi in zh.infolist()],
1025
- expected_zinfos,
1101
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
1026
1102
  )
1027
1103
 
1028
1104
  # check file size
@@ -1078,7 +1154,7 @@ class AbstractRepackTests(RepackHelperMixin):
1078
1154
  fh.write(b'dummy ')
1079
1155
  fh.write(fz.read())
1080
1156
  with zipfile.ZipFile(TESTFN) as zh:
1081
- expected_zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
1157
+ expected_zinfos = list(zh.infolist())
1082
1158
  expected_size = os.path.getsize(TESTFN)
1083
1159
 
1084
1160
  # do the removal and check the result
@@ -1095,7 +1171,7 @@ class AbstractRepackTests(RepackHelperMixin):
1095
1171
  # check infolist
1096
1172
  self.assertEqual(
1097
1173
  [ComparableZipInfo(zi) for zi in zh.infolist()],
1098
- expected_zinfos,
1174
+ [ComparableZipInfo(zi) for zi in expected_zinfos],
1099
1175
  )
1100
1176
 
1101
1177
  # check file size
@@ -1392,7 +1468,7 @@ class ZipRepackerTests(unittest.TestCase):
1392
1468
  fz = io.BytesIO()
1393
1469
  f = Unseekable(fz) if dd else fz
1394
1470
  cm = (mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig)
1395
- if not dd_sig else nullcontext())
1471
+ if not dd_sig else contextlib.nullcontext())
1396
1472
  with zipfile.ZipFile(f, 'w', compression=compression) as zh:
1397
1473
  with cm:
1398
1474
  with zh.open(arcname, 'w', force_zip64=force_zip64) as fh:
@@ -18,16 +18,19 @@ except ImportError:
18
18
  # polyfill for Python < 3.12
19
19
  from test.test_zipfile import Unseekable, requires_zlib
20
20
 
21
- ENABLED_RESOURCES = set(os.environ.get("TEST_RESOURCES", "").split(","))
22
-
23
- def requires(resource_name):
21
+ def requires_resource(res):
22
+ if not hasattr(requires_resource, '_resources'):
23
+ requires_resource._resources = set(os.environ.get("TEST_RESOURCES", "").split(","))
24
24
  return unittest.skipUnless(
25
- resource_name in ENABLED_RESOURCES,
26
- f"requires resource: {resource_name!r} (set envvar TEST_RESOURCES)"
25
+ res in requires_resource._resources,
26
+ f"requires resource {res!r} in envvar TEST_RESOURCES"
27
27
  )
28
28
 
29
+ @requires_resource('extralargefile')
30
+ def setUpModule():
31
+ pass
32
+
29
33
 
30
- @requires('extralargefile')
31
34
  class TestRepack(unittest.TestCase):
32
35
  def setUp(self):
33
36
  # Create test data.
File without changes
File without changes
File without changes