pyxllib 0.0.43__py3-none-any.whl → 0.3.197__py3-none-any.whl
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.
- pyxllib/__init__.py +9 -2
- pyxllib/algo/__init__.py +8 -0
- pyxllib/algo/disjoint.py +54 -0
- pyxllib/algo/geo.py +541 -0
- pyxllib/{util/mathlib.py → algo/intervals.py} +172 -36
- pyxllib/algo/matcher.py +389 -0
- pyxllib/algo/newbie.py +166 -0
- pyxllib/algo/pupil.py +629 -0
- pyxllib/algo/shapelylib.py +67 -0
- pyxllib/algo/specialist.py +241 -0
- pyxllib/algo/stat.py +494 -0
- pyxllib/algo/treelib.py +149 -0
- pyxllib/algo/unitlib.py +66 -0
- pyxllib/autogui/__init__.py +5 -0
- pyxllib/autogui/activewin.py +246 -0
- pyxllib/autogui/all.py +9 -0
- pyxllib/autogui/autogui.py +852 -0
- pyxllib/autogui/uiautolib.py +362 -0
- pyxllib/autogui/virtualkey.py +102 -0
- pyxllib/autogui/wechat.py +827 -0
- pyxllib/autogui/wechat_msg.py +421 -0
- pyxllib/autogui/wxautolib.py +84 -0
- pyxllib/cv/__init__.py +1 -11
- pyxllib/cv/expert.py +267 -0
- pyxllib/cv/{imlib.py → imfile.py} +18 -83
- pyxllib/cv/imhash.py +39 -0
- pyxllib/cv/pupil.py +9 -0
- pyxllib/cv/rgbfmt.py +1525 -0
- pyxllib/cv/slidercaptcha.py +137 -0
- pyxllib/cv/trackbartools.py +163 -49
- pyxllib/cv/xlcvlib.py +1040 -0
- pyxllib/cv/xlpillib.py +423 -0
- pyxllib/data/__init__.py +0 -0
- pyxllib/data/echarts.py +240 -0
- pyxllib/data/jsonlib.py +89 -0
- pyxllib/{util/oss2_.py → data/oss.py} +11 -9
- pyxllib/data/pglib.py +1127 -0
- pyxllib/data/sqlite.py +568 -0
- pyxllib/{util → data}/sqllib.py +13 -31
- pyxllib/ext/JLineViewer.py +505 -0
- pyxllib/ext/__init__.py +6 -0
- pyxllib/{util → ext}/demolib.py +119 -35
- pyxllib/ext/drissionlib.py +277 -0
- pyxllib/ext/kq5034lib.py +12 -0
- pyxllib/{util/main.py → ext/old.py} +122 -284
- pyxllib/ext/qt.py +449 -0
- pyxllib/ext/robustprocfile.py +497 -0
- pyxllib/ext/seleniumlib.py +76 -0
- pyxllib/{util/tklib.py → ext/tk.py} +10 -11
- pyxllib/ext/unixlib.py +827 -0
- pyxllib/ext/utools.py +351 -0
- pyxllib/{util/webhooklib.py → ext/webhook.py} +45 -17
- pyxllib/ext/win32lib.py +40 -0
- pyxllib/ext/wjxlib.py +88 -0
- pyxllib/ext/wpsapi.py +124 -0
- pyxllib/ext/xlwork.py +9 -0
- pyxllib/ext/yuquelib.py +1105 -0
- pyxllib/file/__init__.py +17 -0
- pyxllib/file/docxlib.py +761 -0
- pyxllib/{util → file}/gitlib.py +40 -27
- pyxllib/file/libreoffice.py +165 -0
- pyxllib/file/movielib.py +148 -0
- pyxllib/file/newbie.py +10 -0
- pyxllib/file/onenotelib.py +1469 -0
- pyxllib/file/packlib/__init__.py +330 -0
- pyxllib/{util → file/packlib}/zipfile.py +598 -195
- pyxllib/file/pdflib.py +426 -0
- pyxllib/file/pupil.py +185 -0
- pyxllib/file/specialist/__init__.py +685 -0
- pyxllib/{basic/_5_dirlib.py → file/specialist/dirlib.py} +364 -93
- pyxllib/file/specialist/download.py +193 -0
- pyxllib/file/specialist/filelib.py +2829 -0
- pyxllib/file/xlsxlib.py +3131 -0
- pyxllib/file/xlsyncfile.py +341 -0
- pyxllib/prog/__init__.py +5 -0
- pyxllib/prog/cachetools.py +64 -0
- pyxllib/prog/deprecatedlib.py +233 -0
- pyxllib/prog/filelock.py +42 -0
- pyxllib/prog/ipyexec.py +253 -0
- pyxllib/prog/multiprogs.py +940 -0
- pyxllib/prog/newbie.py +451 -0
- pyxllib/prog/pupil.py +1197 -0
- pyxllib/{sitepackages.py → prog/sitepackages.py} +5 -3
- pyxllib/prog/specialist/__init__.py +391 -0
- pyxllib/prog/specialist/bc.py +203 -0
- pyxllib/prog/specialist/browser.py +497 -0
- pyxllib/prog/specialist/common.py +347 -0
- pyxllib/prog/specialist/datetime.py +199 -0
- pyxllib/prog/specialist/tictoc.py +240 -0
- pyxllib/prog/specialist/xllog.py +180 -0
- pyxllib/prog/xlosenv.py +108 -0
- pyxllib/stdlib/__init__.py +17 -0
- pyxllib/{util → stdlib}/tablepyxl/__init__.py +1 -3
- pyxllib/{util → stdlib}/tablepyxl/style.py +1 -1
- pyxllib/{util → stdlib}/tablepyxl/tablepyxl.py +2 -4
- pyxllib/text/__init__.py +8 -0
- pyxllib/text/ahocorasick.py +39 -0
- pyxllib/text/airscript.js +744 -0
- pyxllib/text/charclasslib.py +121 -0
- pyxllib/text/jiebalib.py +267 -0
- pyxllib/text/jinjalib.py +32 -0
- pyxllib/text/jsa_ai_prompt.md +271 -0
- pyxllib/text/jscode.py +922 -0
- pyxllib/text/latex/__init__.py +158 -0
- pyxllib/text/levenshtein.py +303 -0
- pyxllib/text/nestenv.py +1215 -0
- pyxllib/text/newbie.py +300 -0
- pyxllib/text/pupil/__init__.py +8 -0
- pyxllib/text/pupil/common.py +1121 -0
- pyxllib/text/pupil/xlalign.py +326 -0
- pyxllib/text/pycode.py +47 -0
- pyxllib/text/specialist/__init__.py +8 -0
- pyxllib/text/specialist/common.py +112 -0
- pyxllib/text/specialist/ptag.py +186 -0
- pyxllib/text/spellchecker.py +172 -0
- pyxllib/text/templates/echart_base.html +11 -0
- pyxllib/text/templates/highlight_code.html +17 -0
- pyxllib/text/templates/latex_editor.html +103 -0
- pyxllib/text/vbacode.py +17 -0
- pyxllib/text/xmllib.py +747 -0
- pyxllib/xl.py +39 -0
- pyxllib/xlcv.py +17 -0
- pyxllib-0.3.197.dist-info/METADATA +48 -0
- pyxllib-0.3.197.dist-info/RECORD +126 -0
- {pyxllib-0.0.43.dist-info → pyxllib-0.3.197.dist-info}/WHEEL +4 -5
- pyxllib/basic/_1_strlib.py +0 -945
- pyxllib/basic/_2_timelib.py +0 -488
- pyxllib/basic/_3_pathlib.py +0 -916
- pyxllib/basic/_4_loglib.py +0 -419
- pyxllib/basic/__init__.py +0 -54
- pyxllib/basic/arrow_.py +0 -250
- pyxllib/basic/chardet_.py +0 -66
- pyxllib/basic/dirlib.py +0 -529
- pyxllib/basic/dprint.py +0 -202
- pyxllib/basic/extension.py +0 -12
- pyxllib/basic/judge.py +0 -31
- pyxllib/basic/log.py +0 -204
- pyxllib/basic/pathlib_.py +0 -705
- pyxllib/basic/pytictoc.py +0 -102
- pyxllib/basic/qiniu_.py +0 -61
- pyxllib/basic/strlib.py +0 -761
- pyxllib/basic/timer.py +0 -132
- pyxllib/cv/cv.py +0 -834
- pyxllib/cv/cvlib/_1_geo.py +0 -543
- pyxllib/cv/cvlib/_2_cvprcs.py +0 -309
- pyxllib/cv/cvlib/_2_imgproc.py +0 -594
- pyxllib/cv/cvlib/_3_pilprcs.py +0 -80
- pyxllib/cv/cvlib/_4_cvimg.py +0 -211
- pyxllib/cv/cvlib/__init__.py +0 -10
- pyxllib/cv/debugtools.py +0 -82
- pyxllib/cv/fitz_.py +0 -300
- pyxllib/cv/installer.py +0 -42
- pyxllib/debug/_0_installer.py +0 -38
- pyxllib/debug/_1_typelib.py +0 -277
- pyxllib/debug/_2_chrome.py +0 -198
- pyxllib/debug/_3_showdir.py +0 -161
- pyxllib/debug/_4_bcompare.py +0 -140
- pyxllib/debug/__init__.py +0 -49
- pyxllib/debug/bcompare.py +0 -132
- pyxllib/debug/chrome.py +0 -198
- pyxllib/debug/installer.py +0 -38
- pyxllib/debug/showdir.py +0 -158
- pyxllib/debug/typelib.py +0 -278
- pyxllib/image/__init__.py +0 -12
- pyxllib/torch/__init__.py +0 -20
- pyxllib/torch/modellib.py +0 -37
- pyxllib/torch/trainlib.py +0 -344
- pyxllib/util/__init__.py +0 -20
- pyxllib/util/aip_.py +0 -141
- pyxllib/util/casiadb.py +0 -59
- pyxllib/util/excellib.py +0 -495
- pyxllib/util/filelib.py +0 -612
- pyxllib/util/jsondata.py +0 -27
- pyxllib/util/jsondata2.py +0 -92
- pyxllib/util/labelmelib.py +0 -139
- pyxllib/util/onepy/__init__.py +0 -29
- pyxllib/util/onepy/onepy.py +0 -574
- pyxllib/util/onepy/onmanager.py +0 -170
- pyxllib/util/pyautogui_.py +0 -219
- pyxllib/util/textlib.py +0 -1305
- pyxllib/util/unorder.py +0 -22
- pyxllib/util/xmllib.py +0 -639
- pyxllib-0.0.43.dist-info/METADATA +0 -39
- pyxllib-0.0.43.dist-info/RECORD +0 -80
- pyxllib-0.0.43.dist-info/top_level.txt +0 -1
- {pyxllib-0.0.43.dist-info → pyxllib-0.3.197.dist-info/licenses}/LICENSE +0 -0
@@ -3,21 +3,20 @@ Read and write ZIP files.
|
|
3
3
|
|
4
4
|
XXX references to utf-8 need further investigation.
|
5
5
|
"""
|
6
|
+
import binascii
|
7
|
+
import functools
|
8
|
+
import importlib.util
|
6
9
|
import io
|
10
|
+
import itertools
|
7
11
|
import os
|
8
|
-
import
|
9
|
-
import importlib.util
|
10
|
-
import sys
|
11
|
-
import time
|
12
|
-
import stat
|
12
|
+
import posixpath
|
13
13
|
import shutil
|
14
|
+
import stat
|
14
15
|
import struct
|
15
|
-
import
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
except ImportError:
|
20
|
-
import dummy_threading as threading
|
16
|
+
import sys
|
17
|
+
import threading
|
18
|
+
import time
|
19
|
+
import contextlib
|
21
20
|
|
22
21
|
try:
|
23
22
|
import zlib # We may need its compression method
|
@@ -38,7 +37,8 @@ except ImportError:
|
|
38
37
|
|
39
38
|
__all__ = ["BadZipFile", "BadZipfile", "error",
|
40
39
|
"ZIP_STORED", "ZIP_DEFLATED", "ZIP_BZIP2", "ZIP_LZMA",
|
41
|
-
"is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile"
|
40
|
+
"is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile",
|
41
|
+
"Path"]
|
42
42
|
|
43
43
|
class BadZipFile(Exception):
|
44
44
|
pass
|
@@ -164,6 +164,29 @@ _CD64_NUMBER_ENTRIES_TOTAL = 7
|
|
164
164
|
_CD64_DIRECTORY_SIZE = 8
|
165
165
|
_CD64_OFFSET_START_CENTDIR = 9
|
166
166
|
|
167
|
+
_DD_SIGNATURE = 0x08074b50
|
168
|
+
|
169
|
+
_EXTRA_FIELD_STRUCT = struct.Struct('<HH')
|
170
|
+
|
171
|
+
def _strip_extra(extra, xids):
|
172
|
+
# Remove Extra Fields with specified IDs.
|
173
|
+
unpack = _EXTRA_FIELD_STRUCT.unpack
|
174
|
+
modified = False
|
175
|
+
buffer = []
|
176
|
+
start = i = 0
|
177
|
+
while i + 4 <= len(extra):
|
178
|
+
xid, xlen = unpack(extra[i : i + 4])
|
179
|
+
j = i + 4 + xlen
|
180
|
+
if xid in xids:
|
181
|
+
if i != start:
|
182
|
+
buffer.append(extra[start : i])
|
183
|
+
start = j
|
184
|
+
modified = True
|
185
|
+
i = j
|
186
|
+
if not modified:
|
187
|
+
return extra
|
188
|
+
return b''.join(buffer)
|
189
|
+
|
167
190
|
def _check_zipfile(fp):
|
168
191
|
try:
|
169
192
|
if _EndRecData(fp):
|
@@ -206,7 +229,7 @@ def _EndRecData64(fpin, offset, endrec):
|
|
206
229
|
if sig != stringEndArchive64Locator:
|
207
230
|
return endrec
|
208
231
|
|
209
|
-
if diskno != 0 or disks
|
232
|
+
if diskno != 0 or disks > 1:
|
210
233
|
raise BadZipFile("zipfiles that span multiple disks are not supported")
|
211
234
|
|
212
235
|
# Assume no 'zip64 extensible data'
|
@@ -300,6 +323,7 @@ class ZipInfo (object):
|
|
300
323
|
'filename',
|
301
324
|
'date_time',
|
302
325
|
'compress_type',
|
326
|
+
'_compresslevel',
|
303
327
|
'comment',
|
304
328
|
'extra',
|
305
329
|
'create_system',
|
@@ -339,6 +363,7 @@ class ZipInfo (object):
|
|
339
363
|
|
340
364
|
# Standard values:
|
341
365
|
self.compress_type = ZIP_STORED # Type of compression for the file
|
366
|
+
self._compresslevel = None # Level for the compressor
|
342
367
|
self.comment = b"" # Comment for each file
|
343
368
|
self.extra = b"" # ZIP extra data
|
344
369
|
if sys.platform == 'win32':
|
@@ -382,7 +407,7 @@ class ZipInfo (object):
|
|
382
407
|
return ''.join(result)
|
383
408
|
|
384
409
|
def FileHeader(self, zip64=None):
|
385
|
-
"""Return the per-file header as a
|
410
|
+
"""Return the per-file header as a bytes object."""
|
386
411
|
dt = self.date_time
|
387
412
|
dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
|
388
413
|
dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
|
@@ -439,7 +464,9 @@ class ZipInfo (object):
|
|
439
464
|
unpack = struct.unpack
|
440
465
|
while len(extra) >= 4:
|
441
466
|
tp, ln = unpack('<HH', extra[:4])
|
442
|
-
if
|
467
|
+
if ln+4 > len(extra):
|
468
|
+
raise BadZipFile("Corrupt extra field %04x (size=%d)" % (tp, ln))
|
469
|
+
if tp == 0x0001:
|
443
470
|
if ln >= 24:
|
444
471
|
counts = unpack('<QQQ', extra[4:28])
|
445
472
|
elif ln == 16:
|
@@ -455,14 +482,26 @@ class ZipInfo (object):
|
|
455
482
|
|
456
483
|
# ZIP64 extension (large files and/or large archives)
|
457
484
|
if self.file_size in (0xffffffffffffffff, 0xffffffff):
|
485
|
+
if len(counts) <= idx:
|
486
|
+
raise BadZipFile(
|
487
|
+
"Corrupt zip64 extra field. File size not found."
|
488
|
+
)
|
458
489
|
self.file_size = counts[idx]
|
459
490
|
idx += 1
|
460
491
|
|
461
492
|
if self.compress_size == 0xFFFFFFFF:
|
493
|
+
if len(counts) <= idx:
|
494
|
+
raise BadZipFile(
|
495
|
+
"Corrupt zip64 extra field. Compress size not found."
|
496
|
+
)
|
462
497
|
self.compress_size = counts[idx]
|
463
498
|
idx += 1
|
464
499
|
|
465
500
|
if self.header_offset == 0xffffffff:
|
501
|
+
if len(counts) <= idx:
|
502
|
+
raise BadZipFile(
|
503
|
+
"Corrupt zip64 extra field. Header offset not found."
|
504
|
+
)
|
466
505
|
old = self.header_offset
|
467
506
|
self.header_offset = counts[idx]
|
468
507
|
idx+=1
|
@@ -470,7 +509,7 @@ class ZipInfo (object):
|
|
470
509
|
extra = extra[ln+4:]
|
471
510
|
|
472
511
|
@classmethod
|
473
|
-
def from_file(cls, filename, arcname=None):
|
512
|
+
def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
|
474
513
|
"""Construct an appropriate ZipInfo for a file on the filesystem.
|
475
514
|
|
476
515
|
filename should be the path to a file or directory on the filesystem.
|
@@ -485,6 +524,10 @@ class ZipInfo (object):
|
|
485
524
|
isdir = stat.S_ISDIR(st.st_mode)
|
486
525
|
mtime = time.localtime(st.st_mtime)
|
487
526
|
date_time = mtime[0:6]
|
527
|
+
if not strict_timestamps and date_time[0] < 1980:
|
528
|
+
date_time = (1980, 1, 1, 0, 0, 0)
|
529
|
+
elif not strict_timestamps and date_time[0] > 2107:
|
530
|
+
date_time = (2107, 12, 31, 23, 59, 59)
|
488
531
|
# Create ZipInfo instance to store file information
|
489
532
|
if arcname is None:
|
490
533
|
arcname = filename
|
@@ -508,65 +551,63 @@ class ZipInfo (object):
|
|
508
551
|
return self.filename[-1] == '/'
|
509
552
|
|
510
553
|
|
511
|
-
|
512
|
-
|
554
|
+
# ZIP encryption uses the CRC32 one-byte primitive for scrambling some
|
555
|
+
# internal keys. We noticed that a direct implementation is faster than
|
556
|
+
# relying on binascii.crc32().
|
513
557
|
|
514
|
-
|
515
|
-
|
516
|
-
|
558
|
+
_crctable = None
|
559
|
+
def _gen_crc(crc):
|
560
|
+
for j in range(8):
|
561
|
+
if crc & 1:
|
562
|
+
crc = (crc >> 1) ^ 0xEDB88320
|
563
|
+
else:
|
564
|
+
crc >>= 1
|
565
|
+
return crc
|
566
|
+
|
567
|
+
# ZIP supports a password-based form of encryption. Even though known
|
568
|
+
# plaintext attacks have been found against it, it is still useful
|
569
|
+
# to be able to get data out of such a file.
|
570
|
+
#
|
571
|
+
# Usage:
|
572
|
+
# zd = _ZipDecrypter(mypwd)
|
573
|
+
# plain_bytes = zd(cypher_bytes)
|
574
|
+
|
575
|
+
def _ZipDecrypter(pwd):
|
576
|
+
key0 = 305419896
|
577
|
+
key1 = 591751049
|
578
|
+
key2 = 878082192
|
579
|
+
|
580
|
+
global _crctable
|
581
|
+
if _crctable is None:
|
582
|
+
_crctable = list(map(_gen_crc, range(256)))
|
583
|
+
crctable = _crctable
|
584
|
+
|
585
|
+
def crc32(ch, crc):
|
586
|
+
"""Compute the CRC32 primitive on one byte."""
|
587
|
+
return (crc >> 8) ^ crctable[(crc ^ ch) & 0xFF]
|
517
588
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
589
|
+
def update_keys(c):
|
590
|
+
nonlocal key0, key1, key2
|
591
|
+
key0 = crc32(c, key0)
|
592
|
+
key1 = (key1 + (key0 & 0xFF)) & 0xFFFFFFFF
|
593
|
+
key1 = (key1 * 134775813 + 1) & 0xFFFFFFFF
|
594
|
+
key2 = crc32(key1 >> 24, key2)
|
523
595
|
|
524
|
-
|
525
|
-
|
596
|
+
for p in pwd:
|
597
|
+
update_keys(p)
|
526
598
|
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
crc = ((crc >> 1) & 0x7FFFFFFF) ^ poly
|
538
|
-
else:
|
539
|
-
crc = ((crc >> 1) & 0x7FFFFFFF)
|
540
|
-
table[i] = crc
|
541
|
-
return table
|
542
|
-
crctable = None
|
599
|
+
def decrypter(data):
|
600
|
+
"""Decrypt a bytes object."""
|
601
|
+
result = bytearray()
|
602
|
+
append = result.append
|
603
|
+
for c in data:
|
604
|
+
k = key2 | 2
|
605
|
+
c ^= ((k * (k^1)) >> 8) & 0xFF
|
606
|
+
update_keys(c)
|
607
|
+
append(c)
|
608
|
+
return bytes(result)
|
543
609
|
|
544
|
-
|
545
|
-
"""Compute the CRC32 primitive on one byte."""
|
546
|
-
return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ch) & 0xff]
|
547
|
-
|
548
|
-
def __init__(self, pwd):
|
549
|
-
if _ZipDecrypter.crctable is None:
|
550
|
-
_ZipDecrypter.crctable = _ZipDecrypter._GenerateCRCTable()
|
551
|
-
self.key0 = 305419896
|
552
|
-
self.key1 = 591751049
|
553
|
-
self.key2 = 878082192
|
554
|
-
for p in pwd:
|
555
|
-
self._UpdateKeys(p)
|
556
|
-
|
557
|
-
def _UpdateKeys(self, c):
|
558
|
-
self.key0 = self._crc32(c, self.key0)
|
559
|
-
self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295
|
560
|
-
self.key1 = (self.key1 * 134775813 + 1) & 4294967295
|
561
|
-
self.key2 = self._crc32((self.key1 >> 24) & 255, self.key2)
|
562
|
-
|
563
|
-
def __call__(self, c):
|
564
|
-
"""Decrypt a single character."""
|
565
|
-
assert isinstance(c, int)
|
566
|
-
k = self.key2 | 2
|
567
|
-
c = c ^ (((k * (k^1)) >> 8) & 255)
|
568
|
-
self._UpdateKeys(c)
|
569
|
-
return c
|
610
|
+
return decrypter
|
570
611
|
|
571
612
|
|
572
613
|
class LZMACompressor:
|
@@ -659,12 +700,16 @@ def _check_compression(compression):
|
|
659
700
|
raise NotImplementedError("That compression method is not supported")
|
660
701
|
|
661
702
|
|
662
|
-
def _get_compressor(compress_type):
|
703
|
+
def _get_compressor(compress_type, compresslevel=None):
|
663
704
|
if compress_type == ZIP_DEFLATED:
|
664
|
-
|
665
|
-
|
705
|
+
if compresslevel is not None:
|
706
|
+
return zlib.compressobj(compresslevel, zlib.DEFLATED, -15)
|
707
|
+
return zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)
|
666
708
|
elif compress_type == ZIP_BZIP2:
|
709
|
+
if compresslevel is not None:
|
710
|
+
return bz2.BZ2Compressor(compresslevel)
|
667
711
|
return bz2.BZ2Compressor()
|
712
|
+
# compresslevel is ignored for ZIP_LZMA
|
668
713
|
elif compress_type == ZIP_LZMA:
|
669
714
|
return LZMACompressor()
|
670
715
|
else:
|
@@ -672,6 +717,7 @@ def _get_compressor(compress_type):
|
|
672
717
|
|
673
718
|
|
674
719
|
def _get_decompressor(compress_type):
|
720
|
+
_check_compression(compress_type)
|
675
721
|
if compress_type == ZIP_STORED:
|
676
722
|
return None
|
677
723
|
elif compress_type == ZIP_DEFLATED:
|
@@ -695,6 +741,18 @@ class _SharedFile:
|
|
695
741
|
self._close = close
|
696
742
|
self._lock = lock
|
697
743
|
self._writing = writing
|
744
|
+
self.seekable = file.seekable
|
745
|
+
self.tell = file.tell
|
746
|
+
|
747
|
+
def seek(self, offset, whence=0):
|
748
|
+
with self._lock:
|
749
|
+
if self._writing():
|
750
|
+
raise ValueError("Can't reposition in the ZIP file while "
|
751
|
+
"there is an open writing handle on it. "
|
752
|
+
"Close the writing handle before trying to read.")
|
753
|
+
self._file.seek(offset, whence)
|
754
|
+
self._pos = self._file.tell()
|
755
|
+
return self._pos
|
698
756
|
|
699
757
|
def read(self, n=-1):
|
700
758
|
with self._lock:
|
@@ -745,10 +803,13 @@ class ZipExtFile(io.BufferedIOBase):
|
|
745
803
|
# Read from compressed files in 4k blocks.
|
746
804
|
MIN_READ_SIZE = 4096
|
747
805
|
|
748
|
-
|
806
|
+
# Chunk size to read during seek
|
807
|
+
MAX_SEEK_READ = 1 << 24
|
808
|
+
|
809
|
+
def __init__(self, fileobj, mode, zipinfo, pwd=None,
|
749
810
|
close_fileobj=False):
|
750
811
|
self._fileobj = fileobj
|
751
|
-
self.
|
812
|
+
self._pwd = pwd
|
752
813
|
self._close_fileobj = close_fileobj
|
753
814
|
|
754
815
|
self._compress_type = zipinfo.compress_type
|
@@ -763,11 +824,6 @@ class ZipExtFile(io.BufferedIOBase):
|
|
763
824
|
|
764
825
|
self.newlines = None
|
765
826
|
|
766
|
-
# Adjust read size for encrypted files since the first 12 bytes
|
767
|
-
# are for the encryption/password information.
|
768
|
-
if self._decrypter is not None:
|
769
|
-
self._compress_left -= 12
|
770
|
-
|
771
827
|
self.mode = mode
|
772
828
|
self.name = zipinfo.filename
|
773
829
|
|
@@ -777,6 +833,41 @@ class ZipExtFile(io.BufferedIOBase):
|
|
777
833
|
else:
|
778
834
|
self._expected_crc = None
|
779
835
|
|
836
|
+
self._seekable = False
|
837
|
+
try:
|
838
|
+
if fileobj.seekable():
|
839
|
+
self._orig_compress_start = fileobj.tell()
|
840
|
+
self._orig_compress_size = zipinfo.compress_size
|
841
|
+
self._orig_file_size = zipinfo.file_size
|
842
|
+
self._orig_start_crc = self._running_crc
|
843
|
+
self._seekable = True
|
844
|
+
except AttributeError:
|
845
|
+
pass
|
846
|
+
|
847
|
+
self._decrypter = None
|
848
|
+
if pwd:
|
849
|
+
if zipinfo.flag_bits & 0x8:
|
850
|
+
# compare against the file type from extended local headers
|
851
|
+
check_byte = (zipinfo._raw_time >> 8) & 0xff
|
852
|
+
else:
|
853
|
+
# compare against the CRC otherwise
|
854
|
+
check_byte = (zipinfo.CRC >> 24) & 0xff
|
855
|
+
h = self._init_decrypter()
|
856
|
+
if h != check_byte:
|
857
|
+
raise RuntimeError("Bad password for file %r" % zipinfo.orig_filename)
|
858
|
+
|
859
|
+
|
860
|
+
def _init_decrypter(self):
|
861
|
+
self._decrypter = _ZipDecrypter(self._pwd)
|
862
|
+
# The first 12 bytes in the cypher stream is an encryption header
|
863
|
+
# used to strengthen the algorithm. The first 11 bytes are
|
864
|
+
# completely random, while the 12th contains the MSB of the CRC,
|
865
|
+
# or the MSB of the file time depending on the header type
|
866
|
+
# and is used to check the correctness of the password.
|
867
|
+
header = self._fileobj.read(12)
|
868
|
+
self._compress_left -= 12
|
869
|
+
return self._decrypter(header)[11]
|
870
|
+
|
780
871
|
def __repr__(self):
|
781
872
|
result = ['<%s.%s' % (self.__class__.__module__,
|
782
873
|
self.__class__.__qualname__)]
|
@@ -825,7 +916,7 @@ class ZipExtFile(io.BufferedIOBase):
|
|
825
916
|
|
826
917
|
def read(self, n=-1):
|
827
918
|
"""Read and return up to n bytes.
|
828
|
-
If the argument is omitted, None, or negative, data is read and returned until EOF is reached
|
919
|
+
If the argument is omitted, None, or negative, data is read and returned until EOF is reached.
|
829
920
|
"""
|
830
921
|
if n is None or n < 0:
|
831
922
|
buf = self._readbuffer[self._offset:]
|
@@ -952,7 +1043,7 @@ class ZipExtFile(io.BufferedIOBase):
|
|
952
1043
|
raise EOFError
|
953
1044
|
|
954
1045
|
if self._decrypter is not None:
|
955
|
-
data =
|
1046
|
+
data = self._decrypter(data)
|
956
1047
|
return data
|
957
1048
|
|
958
1049
|
def close(self):
|
@@ -962,13 +1053,71 @@ class ZipExtFile(io.BufferedIOBase):
|
|
962
1053
|
finally:
|
963
1054
|
super().close()
|
964
1055
|
|
1056
|
+
def seekable(self):
|
1057
|
+
return self._seekable
|
1058
|
+
|
1059
|
+
def seek(self, offset, whence=0):
|
1060
|
+
if not self._seekable:
|
1061
|
+
raise io.UnsupportedOperation("underlying stream is not seekable")
|
1062
|
+
curr_pos = self.tell()
|
1063
|
+
if whence == 0: # Seek from start of file
|
1064
|
+
new_pos = offset
|
1065
|
+
elif whence == 1: # Seek from current position
|
1066
|
+
new_pos = curr_pos + offset
|
1067
|
+
elif whence == 2: # Seek from EOF
|
1068
|
+
new_pos = self._orig_file_size + offset
|
1069
|
+
else:
|
1070
|
+
raise ValueError("whence must be os.SEEK_SET (0), "
|
1071
|
+
"os.SEEK_CUR (1), or os.SEEK_END (2)")
|
1072
|
+
|
1073
|
+
if new_pos > self._orig_file_size:
|
1074
|
+
new_pos = self._orig_file_size
|
1075
|
+
|
1076
|
+
if new_pos < 0:
|
1077
|
+
new_pos = 0
|
1078
|
+
|
1079
|
+
read_offset = new_pos - curr_pos
|
1080
|
+
buff_offset = read_offset + self._offset
|
1081
|
+
|
1082
|
+
if buff_offset >= 0 and buff_offset < len(self._readbuffer):
|
1083
|
+
# Just move the _offset index if the new position is in the _readbuffer
|
1084
|
+
self._offset = buff_offset
|
1085
|
+
read_offset = 0
|
1086
|
+
elif read_offset < 0:
|
1087
|
+
# Position is before the current position. Reset the ZipExtFile
|
1088
|
+
self._fileobj.seek(self._orig_compress_start)
|
1089
|
+
self._running_crc = self._orig_start_crc
|
1090
|
+
self._compress_left = self._orig_compress_size
|
1091
|
+
self._left = self._orig_file_size
|
1092
|
+
self._readbuffer = b''
|
1093
|
+
self._offset = 0
|
1094
|
+
self._decompressor = _get_decompressor(self._compress_type)
|
1095
|
+
self._eof = False
|
1096
|
+
read_offset = new_pos
|
1097
|
+
if self._decrypter is not None:
|
1098
|
+
self._init_decrypter()
|
1099
|
+
|
1100
|
+
while read_offset > 0:
|
1101
|
+
read_len = min(self.MAX_SEEK_READ, read_offset)
|
1102
|
+
self.read(read_len)
|
1103
|
+
read_offset -= read_len
|
1104
|
+
|
1105
|
+
return self.tell()
|
1106
|
+
|
1107
|
+
def tell(self):
|
1108
|
+
if not self._seekable:
|
1109
|
+
raise io.UnsupportedOperation("underlying stream is not seekable")
|
1110
|
+
filepos = self._orig_file_size - self._left - len(self._readbuffer) + self._offset
|
1111
|
+
return filepos
|
1112
|
+
|
965
1113
|
|
966
1114
|
class _ZipWriteFile(io.BufferedIOBase):
|
967
1115
|
def __init__(self, zf, zinfo, zip64):
|
968
1116
|
self._zinfo = zinfo
|
969
1117
|
self._zip64 = zip64
|
970
1118
|
self._zipfile = zf
|
971
|
-
self._compressor = _get_compressor(zinfo.compress_type
|
1119
|
+
self._compressor = _get_compressor(zinfo.compress_type,
|
1120
|
+
zinfo._compresslevel)
|
972
1121
|
self._file_size = 0
|
973
1122
|
self._compress_size = 0
|
974
1123
|
self._crc = 0
|
@@ -995,52 +1144,56 @@ class _ZipWriteFile(io.BufferedIOBase):
|
|
995
1144
|
def close(self):
|
996
1145
|
if self.closed:
|
997
1146
|
return
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
self.
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
if self.
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1147
|
+
try:
|
1148
|
+
super().close()
|
1149
|
+
# Flush any data from the compressor, and update header info
|
1150
|
+
if self._compressor:
|
1151
|
+
buf = self._compressor.flush()
|
1152
|
+
self._compress_size += len(buf)
|
1153
|
+
self._fileobj.write(buf)
|
1154
|
+
self._zinfo.compress_size = self._compress_size
|
1155
|
+
else:
|
1156
|
+
self._zinfo.compress_size = self._file_size
|
1157
|
+
self._zinfo.CRC = self._crc
|
1158
|
+
self._zinfo.file_size = self._file_size
|
1159
|
+
|
1160
|
+
# Write updated header info
|
1161
|
+
if self._zinfo.flag_bits & 0x08:
|
1162
|
+
# Write CRC and file sizes after the file data
|
1163
|
+
fmt = '<LLQQ' if self._zip64 else '<LLLL'
|
1164
|
+
self._fileobj.write(struct.pack(fmt, _DD_SIGNATURE, self._zinfo.CRC,
|
1165
|
+
self._zinfo.compress_size, self._zinfo.file_size))
|
1166
|
+
self._zipfile.start_dir = self._fileobj.tell()
|
1167
|
+
else:
|
1168
|
+
if not self._zip64:
|
1169
|
+
if self._file_size > ZIP64_LIMIT:
|
1170
|
+
raise RuntimeError(
|
1171
|
+
'File size unexpectedly exceeded ZIP64 limit')
|
1172
|
+
if self._compress_size > ZIP64_LIMIT:
|
1173
|
+
raise RuntimeError(
|
1174
|
+
'Compressed size unexpectedly exceeded ZIP64 limit')
|
1175
|
+
# Seek backwards and write file header (which will now include
|
1176
|
+
# correct CRC and file sizes)
|
1177
|
+
|
1178
|
+
# Preserve current position in file
|
1179
|
+
self._zipfile.start_dir = self._fileobj.tell()
|
1180
|
+
self._fileobj.seek(self._zinfo.header_offset)
|
1181
|
+
self._fileobj.write(self._zinfo.FileHeader(self._zip64))
|
1182
|
+
self._fileobj.seek(self._zipfile.start_dir)
|
1183
|
+
|
1184
|
+
# Successfully written: Add file to our caches
|
1185
|
+
self._zipfile.filelist.append(self._zinfo)
|
1186
|
+
self._zipfile.NameToInfo[self._zinfo.filename] = self._zinfo
|
1187
|
+
finally:
|
1188
|
+
self._zipfile._writing = False
|
1189
|
+
|
1190
|
+
|
1039
1191
|
|
1040
1192
|
class ZipFile:
|
1041
1193
|
""" Class with methods to open, read, write, close, list zip files.
|
1042
1194
|
|
1043
|
-
z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True
|
1195
|
+
z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True,
|
1196
|
+
compresslevel=None)
|
1044
1197
|
|
1045
1198
|
file: Either the path to the file, or a file-like object.
|
1046
1199
|
If it is a path, the file will be opened and closed by ZipFile.
|
@@ -1051,13 +1204,19 @@ class ZipFile:
|
|
1051
1204
|
allowZip64: if True ZipFile will create files with ZIP64 extensions when
|
1052
1205
|
needed, otherwise it will raise an exception when this would
|
1053
1206
|
be necessary.
|
1207
|
+
compresslevel: None (default for the given compression type) or an integer
|
1208
|
+
specifying the level to pass to the compressor.
|
1209
|
+
When using ZIP_STORED or ZIP_LZMA this keyword has no effect.
|
1210
|
+
When using ZIP_DEFLATED integers 0 through 9 are accepted.
|
1211
|
+
When using ZIP_BZIP2 integers 1 through 9 are accepted.
|
1054
1212
|
|
1055
1213
|
"""
|
1056
1214
|
|
1057
1215
|
fp = None # Set here since __del__ checks it
|
1058
1216
|
_windows_illegal_name_trans_table = None
|
1059
1217
|
|
1060
|
-
def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True
|
1218
|
+
def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True,
|
1219
|
+
compresslevel=None, *, strict_timestamps=True):
|
1061
1220
|
"""Open the ZIP file with mode read 'r', write 'w', exclusive create 'x',
|
1062
1221
|
or append 'a'."""
|
1063
1222
|
if mode not in ('r', 'w', 'x', 'a'):
|
@@ -1071,9 +1230,11 @@ class ZipFile:
|
|
1071
1230
|
self.NameToInfo = {} # Find file info given name
|
1072
1231
|
self.filelist = [] # List of ZipInfo instances for archive
|
1073
1232
|
self.compression = compression # Method of compression
|
1233
|
+
self.compresslevel = compresslevel
|
1074
1234
|
self.mode = mode
|
1075
1235
|
self.pwd = None
|
1076
1236
|
self._comment = b''
|
1237
|
+
self._strict_timestamps = strict_timestamps
|
1077
1238
|
|
1078
1239
|
# Check if we were passed a file-like object
|
1079
1240
|
if isinstance(file, os.PathLike):
|
@@ -1210,8 +1371,7 @@ class ZipFile:
|
|
1210
1371
|
filename = filename.decode('utf-8')
|
1211
1372
|
else:
|
1212
1373
|
# Historical ZIP filename encoding
|
1213
|
-
|
1214
|
-
filename = filename.decode('gbk') # code4101�Ĵ�
|
1374
|
+
filename = filename.decode('gbk')
|
1215
1375
|
# Create ZipInfo instance to store file information
|
1216
1376
|
x = ZipInfo(filename)
|
1217
1377
|
x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
|
@@ -1311,7 +1471,7 @@ class ZipFile:
|
|
1311
1471
|
self._didModify = True
|
1312
1472
|
|
1313
1473
|
def read(self, name, pwd=None):
|
1314
|
-
"""Return file bytes
|
1474
|
+
"""Return file bytes for name."""
|
1315
1475
|
with self.open(name, "r", pwd) as fp:
|
1316
1476
|
return fp.read()
|
1317
1477
|
|
@@ -1348,6 +1508,7 @@ class ZipFile:
|
|
1348
1508
|
elif mode == 'w':
|
1349
1509
|
zinfo = ZipInfo(name)
|
1350
1510
|
zinfo.compress_type = self.compression
|
1511
|
+
zinfo._compresslevel = self.compresslevel
|
1351
1512
|
else:
|
1352
1513
|
# Get info object for name
|
1353
1514
|
zinfo = self.getinfo(name)
|
@@ -1385,11 +1546,10 @@ class ZipFile:
|
|
1385
1546
|
# strong encryption
|
1386
1547
|
raise NotImplementedError("strong encryption (flag bit 6)")
|
1387
1548
|
|
1388
|
-
if
|
1549
|
+
if fheader[_FH_GENERAL_PURPOSE_FLAG_BITS] & 0x800:
|
1389
1550
|
# UTF-8 filename
|
1390
1551
|
fname_str = fname.decode("utf-8")
|
1391
1552
|
else:
|
1392
|
-
# fname_str = fname.decode("cp437")
|
1393
1553
|
fname_str = fname.decode("gbk")
|
1394
1554
|
|
1395
1555
|
if fname_str != zinfo.orig_filename:
|
@@ -1399,32 +1559,16 @@ class ZipFile:
|
|
1399
1559
|
|
1400
1560
|
# check for encrypted flag & handle password
|
1401
1561
|
is_encrypted = zinfo.flag_bits & 0x1
|
1402
|
-
zd = None
|
1403
1562
|
if is_encrypted:
|
1404
1563
|
if not pwd:
|
1405
1564
|
pwd = self.pwd
|
1406
1565
|
if not pwd:
|
1407
1566
|
raise RuntimeError("File %r is encrypted, password "
|
1408
1567
|
"required for extraction" % name)
|
1568
|
+
else:
|
1569
|
+
pwd = None
|
1409
1570
|
|
1410
|
-
|
1411
|
-
# The first 12 bytes in the cypher stream is an encryption header
|
1412
|
-
# used to strengthen the algorithm. The first 11 bytes are
|
1413
|
-
# completely random, while the 12th contains the MSB of the CRC,
|
1414
|
-
# or the MSB of the file time depending on the header type
|
1415
|
-
# and is used to check the correctness of the password.
|
1416
|
-
header = zef_file.read(12)
|
1417
|
-
h = list(map(zd, header[0:12]))
|
1418
|
-
if zinfo.flag_bits & 0x8:
|
1419
|
-
# compare against the file type from extended local headers
|
1420
|
-
check_byte = (zinfo._raw_time >> 8) & 0xff
|
1421
|
-
else:
|
1422
|
-
# compare against the CRC otherwise
|
1423
|
-
check_byte = (zinfo.CRC >> 24) & 0xff
|
1424
|
-
if h[11] != check_byte:
|
1425
|
-
raise RuntimeError("Bad password for file %r" % name)
|
1426
|
-
|
1427
|
-
return ZipExtFile(zef_file, mode, zinfo, zd, True)
|
1571
|
+
return ZipExtFile(zef_file, mode, zinfo, pwd, True)
|
1428
1572
|
except:
|
1429
1573
|
zef_file.close()
|
1430
1574
|
raise
|
@@ -1582,7 +1726,8 @@ class ZipFile:
|
|
1582
1726
|
raise LargeZipFile(requires_zip64 +
|
1583
1727
|
" would require ZIP64 extensions")
|
1584
1728
|
|
1585
|
-
def write(self, filename, arcname=None,
|
1729
|
+
def write(self, filename, arcname=None,
|
1730
|
+
compress_type=None, compresslevel=None):
|
1586
1731
|
"""Put the bytes from filename into the archive under the name
|
1587
1732
|
arcname."""
|
1588
1733
|
if not self.fp:
|
@@ -1593,7 +1738,8 @@ class ZipFile:
|
|
1593
1738
|
"Can't write to ZIP archive while an open writing handle exists"
|
1594
1739
|
)
|
1595
1740
|
|
1596
|
-
zinfo = ZipInfo.from_file(filename, arcname
|
1741
|
+
zinfo = ZipInfo.from_file(filename, arcname,
|
1742
|
+
strict_timestamps=self._strict_timestamps)
|
1597
1743
|
|
1598
1744
|
if zinfo.is_dir():
|
1599
1745
|
zinfo.compress_size = 0
|
@@ -1604,6 +1750,11 @@ class ZipFile:
|
|
1604
1750
|
else:
|
1605
1751
|
zinfo.compress_type = self.compression
|
1606
1752
|
|
1753
|
+
if compresslevel is not None:
|
1754
|
+
zinfo._compresslevel = compresslevel
|
1755
|
+
else:
|
1756
|
+
zinfo._compresslevel = self.compresslevel
|
1757
|
+
|
1607
1758
|
if zinfo.is_dir():
|
1608
1759
|
with self._lock:
|
1609
1760
|
if self._seekable:
|
@@ -1624,7 +1775,8 @@ class ZipFile:
|
|
1624
1775
|
with open(filename, "rb") as src, self.open(zinfo, 'w') as dest:
|
1625
1776
|
shutil.copyfileobj(src, dest, 1024*8)
|
1626
1777
|
|
1627
|
-
def writestr(self, zinfo_or_arcname, data,
|
1778
|
+
def writestr(self, zinfo_or_arcname, data,
|
1779
|
+
compress_type=None, compresslevel=None):
|
1628
1780
|
"""Write a file into the archive. The contents is 'data', which
|
1629
1781
|
may be either a 'str' or a 'bytes' instance; if it is a 'str',
|
1630
1782
|
it is encoded as UTF-8 first.
|
@@ -1636,6 +1788,7 @@ class ZipFile:
|
|
1636
1788
|
zinfo = ZipInfo(filename=zinfo_or_arcname,
|
1637
1789
|
date_time=time.localtime(time.time())[:6])
|
1638
1790
|
zinfo.compress_type = self.compression
|
1791
|
+
zinfo._compresslevel = self.compresslevel
|
1639
1792
|
if zinfo.filename[-1] == '/':
|
1640
1793
|
zinfo.external_attr = 0o40775 << 16 # drwxrwxr-x
|
1641
1794
|
zinfo.external_attr |= 0x10 # MS-DOS directory flag
|
@@ -1655,6 +1808,9 @@ class ZipFile:
|
|
1655
1808
|
if compress_type is not None:
|
1656
1809
|
zinfo.compress_type = compress_type
|
1657
1810
|
|
1811
|
+
if compresslevel is not None:
|
1812
|
+
zinfo._compresslevel = compresslevel
|
1813
|
+
|
1658
1814
|
zinfo.file_size = len(data) # Uncompressed size
|
1659
1815
|
with self._lock:
|
1660
1816
|
with self.open(zinfo, mode='w') as dest:
|
@@ -1712,6 +1868,7 @@ class ZipFile:
|
|
1712
1868
|
min_version = 0
|
1713
1869
|
if extra:
|
1714
1870
|
# Append a ZIP64 field to the extra's
|
1871
|
+
extra_data = _strip_extra(extra_data, (1,))
|
1715
1872
|
extra_data = struct.pack(
|
1716
1873
|
'<HH' + 'Q'*len(extra),
|
1717
1874
|
1, 8*len(extra), *extra) + extra_data
|
@@ -1785,6 +1942,8 @@ class ZipFile:
|
|
1785
1942
|
centDirSize, centDirOffset, len(self._comment))
|
1786
1943
|
self.fp.write(endrec)
|
1787
1944
|
self.fp.write(self._comment)
|
1945
|
+
if self.mode == "a":
|
1946
|
+
self.fp.truncate()
|
1788
1947
|
self.fp.flush()
|
1789
1948
|
|
1790
1949
|
def _fpclose(self, fp):
|
@@ -1838,7 +1997,7 @@ class PyZipFile(ZipFile):
|
|
1838
1997
|
if self.debug:
|
1839
1998
|
print("Adding", arcname)
|
1840
1999
|
self.write(fname, arcname)
|
1841
|
-
dirlist = os.listdir(pathname)
|
2000
|
+
dirlist = sorted(os.listdir(pathname))
|
1842
2001
|
dirlist.remove("__init__.py")
|
1843
2002
|
# Add all *.py files and package subdirectories
|
1844
2003
|
for filename in dirlist:
|
@@ -1863,7 +2022,7 @@ class PyZipFile(ZipFile):
|
|
1863
2022
|
# This is NOT a package directory, add its files at top level
|
1864
2023
|
if self.debug:
|
1865
2024
|
print("Adding files from directory", pathname)
|
1866
|
-
for filename in os.listdir(pathname):
|
2025
|
+
for filename in sorted(os.listdir(pathname)):
|
1867
2026
|
path = os.path.join(pathname, filename)
|
1868
2027
|
root, ext = os.path.splitext(filename)
|
1869
2028
|
if ext == ".py":
|
@@ -1968,51 +2127,294 @@ class PyZipFile(ZipFile):
|
|
1968
2127
|
return (fname, archivename)
|
1969
2128
|
|
1970
2129
|
|
1971
|
-
def
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
1985
|
-
|
1986
|
-
|
1987
|
-
|
1988
|
-
|
1989
|
-
print(USAGE)
|
1990
|
-
sys.exit(1)
|
1991
|
-
with ZipFile(args[1], 'r') as zf:
|
1992
|
-
zf.printdir()
|
2130
|
+
def _parents(path):
|
2131
|
+
"""
|
2132
|
+
Given a path with elements separated by
|
2133
|
+
posixpath.sep, generate all parents of that path.
|
2134
|
+
|
2135
|
+
>>> list(_parents('b/d'))
|
2136
|
+
['b']
|
2137
|
+
>>> list(_parents('/b/d/'))
|
2138
|
+
['/b']
|
2139
|
+
>>> list(_parents('b/d/f/'))
|
2140
|
+
['b/d', 'b']
|
2141
|
+
>>> list(_parents('b'))
|
2142
|
+
[]
|
2143
|
+
>>> list(_parents(''))
|
2144
|
+
[]
|
2145
|
+
"""
|
2146
|
+
return itertools.islice(_ancestry(path), 1, None)
|
2147
|
+
|
1993
2148
|
|
1994
|
-
|
1995
|
-
|
1996
|
-
|
1997
|
-
|
1998
|
-
|
2149
|
+
def _ancestry(path):
|
2150
|
+
"""
|
2151
|
+
Given a path with elements separated by
|
2152
|
+
posixpath.sep, generate all elements of that path
|
2153
|
+
|
2154
|
+
>>> list(_ancestry('b/d'))
|
2155
|
+
['b/d', 'b']
|
2156
|
+
>>> list(_ancestry('/b/d/'))
|
2157
|
+
['/b/d', '/b']
|
2158
|
+
>>> list(_ancestry('b/d/f/'))
|
2159
|
+
['b/d/f', 'b/d', 'b']
|
2160
|
+
>>> list(_ancestry('b'))
|
2161
|
+
['b']
|
2162
|
+
>>> list(_ancestry(''))
|
2163
|
+
[]
|
2164
|
+
"""
|
2165
|
+
path = path.rstrip(posixpath.sep)
|
2166
|
+
while path and path != posixpath.sep:
|
2167
|
+
yield path
|
2168
|
+
path, tail = posixpath.split(path)
|
2169
|
+
|
2170
|
+
|
2171
|
+
_dedupe = dict.fromkeys
|
2172
|
+
"""Deduplicate an iterable in original order"""
|
2173
|
+
|
2174
|
+
|
2175
|
+
def _difference(minuend, subtrahend):
|
2176
|
+
"""
|
2177
|
+
Return items in minuend not in subtrahend, retaining order
|
2178
|
+
with O(1) lookup.
|
2179
|
+
"""
|
2180
|
+
return itertools.filterfalse(set(subtrahend).__contains__, minuend)
|
2181
|
+
|
2182
|
+
|
2183
|
+
class CompleteDirs(ZipFile):
|
2184
|
+
"""
|
2185
|
+
A ZipFile subclass that ensures that implied directories
|
2186
|
+
are always included in the namelist.
|
2187
|
+
"""
|
2188
|
+
|
2189
|
+
@staticmethod
|
2190
|
+
def _implied_dirs(names):
|
2191
|
+
parents = itertools.chain.from_iterable(map(_parents, names))
|
2192
|
+
as_dirs = (p + posixpath.sep for p in parents)
|
2193
|
+
return _dedupe(_difference(as_dirs, names))
|
2194
|
+
|
2195
|
+
def namelist(self):
|
2196
|
+
names = super(CompleteDirs, self).namelist()
|
2197
|
+
return names + list(self._implied_dirs(names))
|
2198
|
+
|
2199
|
+
def _name_set(self):
|
2200
|
+
return set(self.namelist())
|
2201
|
+
|
2202
|
+
def resolve_dir(self, name):
|
2203
|
+
"""
|
2204
|
+
If the name represents a directory, return that name
|
2205
|
+
as a directory (with the trailing slash).
|
2206
|
+
"""
|
2207
|
+
names = self._name_set()
|
2208
|
+
dirname = name + '/'
|
2209
|
+
dir_match = name not in names and dirname in names
|
2210
|
+
return dirname if dir_match else name
|
2211
|
+
|
2212
|
+
@classmethod
|
2213
|
+
def make(cls, source):
|
2214
|
+
"""
|
2215
|
+
Given a source (filename or zipfile), return an
|
2216
|
+
appropriate CompleteDirs subclass.
|
2217
|
+
"""
|
2218
|
+
if isinstance(source, CompleteDirs):
|
2219
|
+
return source
|
2220
|
+
|
2221
|
+
if not isinstance(source, ZipFile):
|
2222
|
+
return cls(source)
|
2223
|
+
|
2224
|
+
# Only allow for FastPath when supplied zipfile is read-only
|
2225
|
+
if 'r' not in source.mode:
|
2226
|
+
cls = CompleteDirs
|
2227
|
+
|
2228
|
+
res = cls.__new__(cls)
|
2229
|
+
vars(res).update(vars(source))
|
2230
|
+
return res
|
2231
|
+
|
2232
|
+
|
2233
|
+
class FastLookup(CompleteDirs):
|
2234
|
+
"""
|
2235
|
+
ZipFile subclass to ensure implicit
|
2236
|
+
dirs exist and are resolved rapidly.
|
2237
|
+
"""
|
2238
|
+
def namelist(self):
|
2239
|
+
with contextlib.suppress(AttributeError):
|
2240
|
+
return self.__names
|
2241
|
+
self.__names = super(FastLookup, self).namelist()
|
2242
|
+
return self.__names
|
2243
|
+
|
2244
|
+
def _name_set(self):
|
2245
|
+
with contextlib.suppress(AttributeError):
|
2246
|
+
return self.__lookup
|
2247
|
+
self.__lookup = super(FastLookup, self)._name_set()
|
2248
|
+
return self.__lookup
|
2249
|
+
|
2250
|
+
|
2251
|
+
class Path:
|
2252
|
+
"""
|
2253
|
+
A pathlib-compatible interface for zip files.
|
2254
|
+
|
2255
|
+
Consider a zip file with this structure::
|
2256
|
+
|
2257
|
+
.
|
2258
|
+
├── a.txt
|
2259
|
+
└── b
|
2260
|
+
├── c.txt
|
2261
|
+
└── d
|
2262
|
+
└── e.txt
|
2263
|
+
|
2264
|
+
>>> data = io.BytesIO()
|
2265
|
+
>>> zf = ZipFile(data, 'w')
|
2266
|
+
>>> zf.writestr('a.txt', 'content of a')
|
2267
|
+
>>> zf.writestr('b/c.txt', 'content of c')
|
2268
|
+
>>> zf.writestr('b/d/e.txt', 'content of e')
|
2269
|
+
>>> zf.filename = 'abcde.zip'
|
2270
|
+
|
2271
|
+
Path accepts the zipfile object itself or a filename
|
2272
|
+
|
2273
|
+
>>> root = Path(zf)
|
2274
|
+
|
2275
|
+
From there, several path operations are available.
|
2276
|
+
|
2277
|
+
Directory iteration (including the zip file itself):
|
2278
|
+
|
2279
|
+
>>> a, b = root.iterdir()
|
2280
|
+
>>> a
|
2281
|
+
Path('abcde.zip', 'a.txt')
|
2282
|
+
>>> b
|
2283
|
+
Path('abcde.zip', 'b/')
|
2284
|
+
|
2285
|
+
name property:
|
2286
|
+
|
2287
|
+
>>> b.name
|
2288
|
+
'b'
|
2289
|
+
|
2290
|
+
join with divide operator:
|
2291
|
+
|
2292
|
+
>>> c = b / 'c.txt'
|
2293
|
+
>>> c
|
2294
|
+
Path('abcde.zip', 'b/c.txt')
|
2295
|
+
>>> c.name
|
2296
|
+
'c.txt'
|
2297
|
+
|
2298
|
+
Read text:
|
2299
|
+
|
2300
|
+
>>> c.read_text()
|
2301
|
+
'content of c'
|
2302
|
+
|
2303
|
+
existence:
|
2304
|
+
|
2305
|
+
>>> c.exists()
|
2306
|
+
True
|
2307
|
+
>>> (b / 'missing.txt').exists()
|
2308
|
+
False
|
2309
|
+
|
2310
|
+
Coercion to string:
|
2311
|
+
|
2312
|
+
>>> str(c)
|
2313
|
+
'abcde.zip/b/c.txt'
|
2314
|
+
"""
|
2315
|
+
|
2316
|
+
__repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
|
2317
|
+
|
2318
|
+
def __init__(self, root, at=""):
|
2319
|
+
self.root = FastLookup.make(root)
|
2320
|
+
self.at = at
|
2321
|
+
|
2322
|
+
@property
|
2323
|
+
def open(self):
|
2324
|
+
return functools.partial(self.root.open, self.at)
|
2325
|
+
|
2326
|
+
@property
|
2327
|
+
def name(self):
|
2328
|
+
return posixpath.basename(self.at.rstrip("/"))
|
2329
|
+
|
2330
|
+
def read_text(self, *args, **kwargs):
|
2331
|
+
with self.open() as strm:
|
2332
|
+
return io.TextIOWrapper(strm, *args, **kwargs).read()
|
2333
|
+
|
2334
|
+
def read_bytes(self):
|
2335
|
+
with self.open() as strm:
|
2336
|
+
return strm.read()
|
2337
|
+
|
2338
|
+
def _is_child(self, path):
|
2339
|
+
return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
|
2340
|
+
|
2341
|
+
def _next(self, at):
|
2342
|
+
return Path(self.root, at)
|
2343
|
+
|
2344
|
+
def is_dir(self):
|
2345
|
+
return not self.at or self.at.endswith("/")
|
2346
|
+
|
2347
|
+
def is_file(self):
|
2348
|
+
return not self.is_dir()
|
2349
|
+
|
2350
|
+
def exists(self):
|
2351
|
+
return self.at in self.root._name_set()
|
2352
|
+
|
2353
|
+
def iterdir(self):
|
2354
|
+
if not self.is_dir():
|
2355
|
+
raise ValueError("Can't listdir a file")
|
2356
|
+
subs = map(self._next, self.root.namelist())
|
2357
|
+
return filter(self._is_child, subs)
|
2358
|
+
|
2359
|
+
def __str__(self):
|
2360
|
+
return posixpath.join(self.root.filename, self.at)
|
2361
|
+
|
2362
|
+
def __repr__(self):
|
2363
|
+
return self.__repr.format(self=self)
|
2364
|
+
|
2365
|
+
def joinpath(self, add):
|
2366
|
+
next = posixpath.join(self.at, add)
|
2367
|
+
return self._next(self.root.resolve_dir(next))
|
2368
|
+
|
2369
|
+
__truediv__ = joinpath
|
2370
|
+
|
2371
|
+
@property
|
2372
|
+
def parent(self):
|
2373
|
+
parent_at = posixpath.dirname(self.at.rstrip('/'))
|
2374
|
+
if parent_at:
|
2375
|
+
parent_at += '/'
|
2376
|
+
return self._next(parent_at)
|
2377
|
+
|
2378
|
+
|
2379
|
+
def main(args=None):
|
2380
|
+
import argparse
|
2381
|
+
|
2382
|
+
description = 'A simple command-line interface for zipfile module.'
|
2383
|
+
parser = argparse.ArgumentParser(description=description)
|
2384
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
2385
|
+
group.add_argument('-l', '--list', metavar='<zipfile>',
|
2386
|
+
help='Show listing of a zipfile')
|
2387
|
+
group.add_argument('-e', '--extract', nargs=2,
|
2388
|
+
metavar=('<zipfile>', '<output_dir>'),
|
2389
|
+
help='Extract zipfile into target dir')
|
2390
|
+
group.add_argument('-c', '--create', nargs='+',
|
2391
|
+
metavar=('<name>', '<file>'),
|
2392
|
+
help='Create zipfile from sources')
|
2393
|
+
group.add_argument('-t', '--test', metavar='<zipfile>',
|
2394
|
+
help='Test if a zipfile is valid')
|
2395
|
+
args = parser.parse_args(args)
|
2396
|
+
|
2397
|
+
if args.test is not None:
|
2398
|
+
src = args.test
|
2399
|
+
with ZipFile(src, 'r') as zf:
|
1999
2400
|
badfile = zf.testzip()
|
2000
2401
|
if badfile:
|
2001
2402
|
print("The following enclosed file is corrupted: {!r}".format(badfile))
|
2002
2403
|
print("Done testing")
|
2003
2404
|
|
2004
|
-
elif args
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2405
|
+
elif args.list is not None:
|
2406
|
+
src = args.list
|
2407
|
+
with ZipFile(src, 'r') as zf:
|
2408
|
+
zf.printdir()
|
2008
2409
|
|
2009
|
-
|
2010
|
-
|
2410
|
+
elif args.extract is not None:
|
2411
|
+
src, curdir = args.extract
|
2412
|
+
with ZipFile(src, 'r') as zf:
|
2413
|
+
zf.extractall(curdir)
|
2011
2414
|
|
2012
|
-
elif args
|
2013
|
-
|
2014
|
-
|
2015
|
-
sys.exit(1)
|
2415
|
+
elif args.create is not None:
|
2416
|
+
zip_name = args.create.pop(0)
|
2417
|
+
files = args.create
|
2016
2418
|
|
2017
2419
|
def addToZip(zf, path, zippath):
|
2018
2420
|
if os.path.isfile(path):
|
@@ -2020,13 +2422,13 @@ def main(args = None):
|
|
2020
2422
|
elif os.path.isdir(path):
|
2021
2423
|
if zippath:
|
2022
2424
|
zf.write(path, zippath)
|
2023
|
-
for nm in os.listdir(path):
|
2425
|
+
for nm in sorted(os.listdir(path)):
|
2024
2426
|
addToZip(zf,
|
2025
2427
|
os.path.join(path, nm), os.path.join(zippath, nm))
|
2026
2428
|
# else: ignore
|
2027
2429
|
|
2028
|
-
with ZipFile(
|
2029
|
-
for path in
|
2430
|
+
with ZipFile(zip_name, 'w') as zf:
|
2431
|
+
for path in files:
|
2030
2432
|
zippath = os.path.basename(path)
|
2031
2433
|
if not zippath:
|
2032
2434
|
zippath = os.path.basename(os.path.dirname(path))
|
@@ -2034,5 +2436,6 @@ def main(args = None):
|
|
2034
2436
|
zippath = ''
|
2035
2437
|
addToZip(zf, path, zippath)
|
2036
2438
|
|
2439
|
+
|
2037
2440
|
if __name__ == "__main__":
|
2038
2441
|
main()
|