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.
Files changed (186) hide show
  1. pyxllib/__init__.py +9 -2
  2. pyxllib/algo/__init__.py +8 -0
  3. pyxllib/algo/disjoint.py +54 -0
  4. pyxllib/algo/geo.py +541 -0
  5. pyxllib/{util/mathlib.py → algo/intervals.py} +172 -36
  6. pyxllib/algo/matcher.py +389 -0
  7. pyxllib/algo/newbie.py +166 -0
  8. pyxllib/algo/pupil.py +629 -0
  9. pyxllib/algo/shapelylib.py +67 -0
  10. pyxllib/algo/specialist.py +241 -0
  11. pyxllib/algo/stat.py +494 -0
  12. pyxllib/algo/treelib.py +149 -0
  13. pyxllib/algo/unitlib.py +66 -0
  14. pyxllib/autogui/__init__.py +5 -0
  15. pyxllib/autogui/activewin.py +246 -0
  16. pyxllib/autogui/all.py +9 -0
  17. pyxllib/autogui/autogui.py +852 -0
  18. pyxllib/autogui/uiautolib.py +362 -0
  19. pyxllib/autogui/virtualkey.py +102 -0
  20. pyxllib/autogui/wechat.py +827 -0
  21. pyxllib/autogui/wechat_msg.py +421 -0
  22. pyxllib/autogui/wxautolib.py +84 -0
  23. pyxllib/cv/__init__.py +1 -11
  24. pyxllib/cv/expert.py +267 -0
  25. pyxllib/cv/{imlib.py → imfile.py} +18 -83
  26. pyxllib/cv/imhash.py +39 -0
  27. pyxllib/cv/pupil.py +9 -0
  28. pyxllib/cv/rgbfmt.py +1525 -0
  29. pyxllib/cv/slidercaptcha.py +137 -0
  30. pyxllib/cv/trackbartools.py +163 -49
  31. pyxllib/cv/xlcvlib.py +1040 -0
  32. pyxllib/cv/xlpillib.py +423 -0
  33. pyxllib/data/__init__.py +0 -0
  34. pyxllib/data/echarts.py +240 -0
  35. pyxllib/data/jsonlib.py +89 -0
  36. pyxllib/{util/oss2_.py → data/oss.py} +11 -9
  37. pyxllib/data/pglib.py +1127 -0
  38. pyxllib/data/sqlite.py +568 -0
  39. pyxllib/{util → data}/sqllib.py +13 -31
  40. pyxllib/ext/JLineViewer.py +505 -0
  41. pyxllib/ext/__init__.py +6 -0
  42. pyxllib/{util → ext}/demolib.py +119 -35
  43. pyxllib/ext/drissionlib.py +277 -0
  44. pyxllib/ext/kq5034lib.py +12 -0
  45. pyxllib/{util/main.py → ext/old.py} +122 -284
  46. pyxllib/ext/qt.py +449 -0
  47. pyxllib/ext/robustprocfile.py +497 -0
  48. pyxllib/ext/seleniumlib.py +76 -0
  49. pyxllib/{util/tklib.py → ext/tk.py} +10 -11
  50. pyxllib/ext/unixlib.py +827 -0
  51. pyxllib/ext/utools.py +351 -0
  52. pyxllib/{util/webhooklib.py → ext/webhook.py} +45 -17
  53. pyxllib/ext/win32lib.py +40 -0
  54. pyxllib/ext/wjxlib.py +88 -0
  55. pyxllib/ext/wpsapi.py +124 -0
  56. pyxllib/ext/xlwork.py +9 -0
  57. pyxllib/ext/yuquelib.py +1105 -0
  58. pyxllib/file/__init__.py +17 -0
  59. pyxllib/file/docxlib.py +761 -0
  60. pyxllib/{util → file}/gitlib.py +40 -27
  61. pyxllib/file/libreoffice.py +165 -0
  62. pyxllib/file/movielib.py +148 -0
  63. pyxllib/file/newbie.py +10 -0
  64. pyxllib/file/onenotelib.py +1469 -0
  65. pyxllib/file/packlib/__init__.py +330 -0
  66. pyxllib/{util → file/packlib}/zipfile.py +598 -195
  67. pyxllib/file/pdflib.py +426 -0
  68. pyxllib/file/pupil.py +185 -0
  69. pyxllib/file/specialist/__init__.py +685 -0
  70. pyxllib/{basic/_5_dirlib.py → file/specialist/dirlib.py} +364 -93
  71. pyxllib/file/specialist/download.py +193 -0
  72. pyxllib/file/specialist/filelib.py +2829 -0
  73. pyxllib/file/xlsxlib.py +3131 -0
  74. pyxllib/file/xlsyncfile.py +341 -0
  75. pyxllib/prog/__init__.py +5 -0
  76. pyxllib/prog/cachetools.py +64 -0
  77. pyxllib/prog/deprecatedlib.py +233 -0
  78. pyxllib/prog/filelock.py +42 -0
  79. pyxllib/prog/ipyexec.py +253 -0
  80. pyxllib/prog/multiprogs.py +940 -0
  81. pyxllib/prog/newbie.py +451 -0
  82. pyxllib/prog/pupil.py +1197 -0
  83. pyxllib/{sitepackages.py → prog/sitepackages.py} +5 -3
  84. pyxllib/prog/specialist/__init__.py +391 -0
  85. pyxllib/prog/specialist/bc.py +203 -0
  86. pyxllib/prog/specialist/browser.py +497 -0
  87. pyxllib/prog/specialist/common.py +347 -0
  88. pyxllib/prog/specialist/datetime.py +199 -0
  89. pyxllib/prog/specialist/tictoc.py +240 -0
  90. pyxllib/prog/specialist/xllog.py +180 -0
  91. pyxllib/prog/xlosenv.py +108 -0
  92. pyxllib/stdlib/__init__.py +17 -0
  93. pyxllib/{util → stdlib}/tablepyxl/__init__.py +1 -3
  94. pyxllib/{util → stdlib}/tablepyxl/style.py +1 -1
  95. pyxllib/{util → stdlib}/tablepyxl/tablepyxl.py +2 -4
  96. pyxllib/text/__init__.py +8 -0
  97. pyxllib/text/ahocorasick.py +39 -0
  98. pyxllib/text/airscript.js +744 -0
  99. pyxllib/text/charclasslib.py +121 -0
  100. pyxllib/text/jiebalib.py +267 -0
  101. pyxllib/text/jinjalib.py +32 -0
  102. pyxllib/text/jsa_ai_prompt.md +271 -0
  103. pyxllib/text/jscode.py +922 -0
  104. pyxllib/text/latex/__init__.py +158 -0
  105. pyxllib/text/levenshtein.py +303 -0
  106. pyxllib/text/nestenv.py +1215 -0
  107. pyxllib/text/newbie.py +300 -0
  108. pyxllib/text/pupil/__init__.py +8 -0
  109. pyxllib/text/pupil/common.py +1121 -0
  110. pyxllib/text/pupil/xlalign.py +326 -0
  111. pyxllib/text/pycode.py +47 -0
  112. pyxllib/text/specialist/__init__.py +8 -0
  113. pyxllib/text/specialist/common.py +112 -0
  114. pyxllib/text/specialist/ptag.py +186 -0
  115. pyxllib/text/spellchecker.py +172 -0
  116. pyxllib/text/templates/echart_base.html +11 -0
  117. pyxllib/text/templates/highlight_code.html +17 -0
  118. pyxllib/text/templates/latex_editor.html +103 -0
  119. pyxllib/text/vbacode.py +17 -0
  120. pyxllib/text/xmllib.py +747 -0
  121. pyxllib/xl.py +39 -0
  122. pyxllib/xlcv.py +17 -0
  123. pyxllib-0.3.197.dist-info/METADATA +48 -0
  124. pyxllib-0.3.197.dist-info/RECORD +126 -0
  125. {pyxllib-0.0.43.dist-info → pyxllib-0.3.197.dist-info}/WHEEL +4 -5
  126. pyxllib/basic/_1_strlib.py +0 -945
  127. pyxllib/basic/_2_timelib.py +0 -488
  128. pyxllib/basic/_3_pathlib.py +0 -916
  129. pyxllib/basic/_4_loglib.py +0 -419
  130. pyxllib/basic/__init__.py +0 -54
  131. pyxllib/basic/arrow_.py +0 -250
  132. pyxllib/basic/chardet_.py +0 -66
  133. pyxllib/basic/dirlib.py +0 -529
  134. pyxllib/basic/dprint.py +0 -202
  135. pyxllib/basic/extension.py +0 -12
  136. pyxllib/basic/judge.py +0 -31
  137. pyxllib/basic/log.py +0 -204
  138. pyxllib/basic/pathlib_.py +0 -705
  139. pyxllib/basic/pytictoc.py +0 -102
  140. pyxllib/basic/qiniu_.py +0 -61
  141. pyxllib/basic/strlib.py +0 -761
  142. pyxllib/basic/timer.py +0 -132
  143. pyxllib/cv/cv.py +0 -834
  144. pyxllib/cv/cvlib/_1_geo.py +0 -543
  145. pyxllib/cv/cvlib/_2_cvprcs.py +0 -309
  146. pyxllib/cv/cvlib/_2_imgproc.py +0 -594
  147. pyxllib/cv/cvlib/_3_pilprcs.py +0 -80
  148. pyxllib/cv/cvlib/_4_cvimg.py +0 -211
  149. pyxllib/cv/cvlib/__init__.py +0 -10
  150. pyxllib/cv/debugtools.py +0 -82
  151. pyxllib/cv/fitz_.py +0 -300
  152. pyxllib/cv/installer.py +0 -42
  153. pyxllib/debug/_0_installer.py +0 -38
  154. pyxllib/debug/_1_typelib.py +0 -277
  155. pyxllib/debug/_2_chrome.py +0 -198
  156. pyxllib/debug/_3_showdir.py +0 -161
  157. pyxllib/debug/_4_bcompare.py +0 -140
  158. pyxllib/debug/__init__.py +0 -49
  159. pyxllib/debug/bcompare.py +0 -132
  160. pyxllib/debug/chrome.py +0 -198
  161. pyxllib/debug/installer.py +0 -38
  162. pyxllib/debug/showdir.py +0 -158
  163. pyxllib/debug/typelib.py +0 -278
  164. pyxllib/image/__init__.py +0 -12
  165. pyxllib/torch/__init__.py +0 -20
  166. pyxllib/torch/modellib.py +0 -37
  167. pyxllib/torch/trainlib.py +0 -344
  168. pyxllib/util/__init__.py +0 -20
  169. pyxllib/util/aip_.py +0 -141
  170. pyxllib/util/casiadb.py +0 -59
  171. pyxllib/util/excellib.py +0 -495
  172. pyxllib/util/filelib.py +0 -612
  173. pyxllib/util/jsondata.py +0 -27
  174. pyxllib/util/jsondata2.py +0 -92
  175. pyxllib/util/labelmelib.py +0 -139
  176. pyxllib/util/onepy/__init__.py +0 -29
  177. pyxllib/util/onepy/onepy.py +0 -574
  178. pyxllib/util/onepy/onmanager.py +0 -170
  179. pyxllib/util/pyautogui_.py +0 -219
  180. pyxllib/util/textlib.py +0 -1305
  181. pyxllib/util/unorder.py +0 -22
  182. pyxllib/util/xmllib.py +0 -639
  183. pyxllib-0.0.43.dist-info/METADATA +0 -39
  184. pyxllib-0.0.43.dist-info/RECORD +0 -80
  185. pyxllib-0.0.43.dist-info/top_level.txt +0 -1
  186. {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 re
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 binascii
16
-
17
- try:
18
- import threading
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 != 1:
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 string."""
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 tp == 1:
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
- class _ZipDecrypter:
512
- """Class to handle decryption of files stored within a ZIP archive.
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
- ZIP supports a password-based form of encryption. Even though known
515
- plaintext attacks have been found against it, it is still useful
516
- to be able to get data out of such a file.
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
- Usage:
519
- zd = _ZipDecrypter(mypwd)
520
- plain_char = zd(cypher_char)
521
- plain_text = map(zd, cypher_text)
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
- def _GenerateCRCTable():
525
- """Generate a CRC-32 table.
596
+ for p in pwd:
597
+ update_keys(p)
526
598
 
527
- ZIP encryption uses the CRC32 one-byte primitive for scrambling some
528
- internal keys. We noticed that a direct implementation is faster than
529
- relying on binascii.crc32().
530
- """
531
- poly = 0xedb88320
532
- table = [0] * 256
533
- for i in range(256):
534
- crc = i
535
- for j in range(8):
536
- if crc & 1:
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
- def _crc32(self, ch, crc):
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
- return zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
665
- zlib.DEFLATED, -15)
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
- def __init__(self, fileobj, mode, zipinfo, decrypter=None,
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._decrypter = decrypter
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 = bytes(map(self._decrypter, 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
- super().close()
999
- # Flush any data from the compressor, and update header info
1000
- if self._compressor:
1001
- buf = self._compressor.flush()
1002
- self._compress_size += len(buf)
1003
- self._fileobj.write(buf)
1004
- self._zinfo.compress_size = self._compress_size
1005
- else:
1006
- self._zinfo.compress_size = self._file_size
1007
- self._zinfo.CRC = self._crc
1008
- self._zinfo.file_size = self._file_size
1009
-
1010
- # Write updated header info
1011
- if self._zinfo.flag_bits & 0x08:
1012
- # Write CRC and file sizes after the file data
1013
- fmt = '<LQQ' if self._zip64 else '<LLL'
1014
- self._fileobj.write(struct.pack(fmt, self._zinfo.CRC,
1015
- self._zinfo.compress_size, self._zinfo.file_size))
1016
- self._zipfile.start_dir = self._fileobj.tell()
1017
- else:
1018
- if not self._zip64:
1019
- if self._file_size > ZIP64_LIMIT:
1020
- raise RuntimeError('File size unexpectedly exceeded ZIP64 '
1021
- 'limit')
1022
- if self._compress_size > ZIP64_LIMIT:
1023
- raise RuntimeError('Compressed size unexpectedly exceeded '
1024
- 'ZIP64 limit')
1025
- # Seek backwards and write file header (which will now include
1026
- # correct CRC and file sizes)
1027
-
1028
- # Preserve current position in file
1029
- self._zipfile.start_dir = self._fileobj.tell()
1030
- self._fileobj.seek(self._zinfo.header_offset)
1031
- self._fileobj.write(self._zinfo.FileHeader(self._zip64))
1032
- self._fileobj.seek(self._zipfile.start_dir)
1033
-
1034
- self._zipfile._writing = False
1035
-
1036
- # Successfully written: Add file to our caches
1037
- self._zipfile.filelist.append(self._zinfo)
1038
- self._zipfile.NameToInfo[self._zinfo.filename] = self._zinfo
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
- # filename = filename.decode('cp437')
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 (as a string) for name."""
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 zinfo.flag_bits & 0x800:
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
- zd = _ZipDecrypter(pwd)
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, compress_type=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, compress_type=None):
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 main(args = None):
1972
- import textwrap
1973
- USAGE=textwrap.dedent("""\
1974
- Usage:
1975
- zipfile.py -l zipfile.zip # Show listing of a zipfile
1976
- zipfile.py -t zipfile.zip # Test if a zipfile is valid
1977
- zipfile.py -e zipfile.zip target # Extract zipfile into target dir
1978
- zipfile.py -c zipfile.zip src ... # Create zipfile from sources
1979
- """)
1980
- if args is None:
1981
- args = sys.argv[1:]
1982
-
1983
- if not args or args[0] not in ('-l', '-c', '-e', '-t'):
1984
- print(USAGE)
1985
- sys.exit(1)
1986
-
1987
- if args[0] == '-l':
1988
- if len(args) != 2:
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
- elif args[0] == '-t':
1995
- if len(args) != 2:
1996
- print(USAGE)
1997
- sys.exit(1)
1998
- with ZipFile(args[1], 'r') as zf:
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[0] == '-e':
2005
- if len(args) != 3:
2006
- print(USAGE)
2007
- sys.exit(1)
2405
+ elif args.list is not None:
2406
+ src = args.list
2407
+ with ZipFile(src, 'r') as zf:
2408
+ zf.printdir()
2008
2409
 
2009
- with ZipFile(args[1], 'r') as zf:
2010
- zf.extractall(args[2])
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[0] == '-c':
2013
- if len(args) < 3:
2014
- print(USAGE)
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(args[1], 'w') as zf:
2029
- for path in args[2:]:
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()