zipp.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. # coding: utf-8
  2. from __future__ import division
  3. import io
  4. import sys
  5. import posixpath
  6. import zipfile
  7. import functools
  8. __metaclass__ = type
  9. class Path:
  10. """
  11. A pathlib-compatible interface for zip files.
  12. Consider a zip file with this structure::
  13. .
  14. ├── a.txt
  15. └── b
  16. ├── c.txt
  17. └── d
  18. └── e.txt
  19. >>> data = io.BytesIO()
  20. >>> zf = zipfile.ZipFile(data, 'w')
  21. >>> zf.writestr('a.txt', 'content of a')
  22. >>> zf.writestr('b/c.txt', 'content of c')
  23. >>> zf.writestr('b/d/e.txt', 'content of e')
  24. >>> zf.filename = 'abcde.zip'
  25. Path accepts the zipfile object itself or a filename
  26. >>> root = Path(zf)
  27. From there, several path operations are available.
  28. Directory iteration (including the zip file itself):
  29. >>> a, b = root.iterdir()
  30. >>> a
  31. Path('abcde.zip', 'a.txt')
  32. >>> b
  33. Path('abcde.zip', 'b/')
  34. name property:
  35. >>> b.name
  36. 'b'
  37. join with divide operator:
  38. >>> c = b / 'c.txt'
  39. >>> c
  40. Path('abcde.zip', 'b/c.txt')
  41. >>> c.name
  42. 'c.txt'
  43. Read text:
  44. >>> c.read_text()
  45. 'content of c'
  46. existence:
  47. >>> c.exists()
  48. True
  49. >>> (b / 'missing.txt').exists()
  50. False
  51. Coersion to string:
  52. >>> str(c)
  53. 'abcde.zip/b/c.txt'
  54. """
  55. __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
  56. def __init__(self, root, at=""):
  57. self.root = (
  58. root
  59. if isinstance(root, zipfile.ZipFile)
  60. else zipfile.ZipFile(self._pathlib_compat(root))
  61. )
  62. self.at = at
  63. @staticmethod
  64. def _pathlib_compat(path):
  65. """
  66. For path-like objects, convert to a filename for compatibility
  67. on Python 3.6.1 and earlier.
  68. """
  69. try:
  70. return path.__fspath__()
  71. except AttributeError:
  72. return str(path)
  73. @property
  74. def open(self):
  75. return functools.partial(self.root.open, self.at)
  76. @property
  77. def name(self):
  78. return posixpath.basename(self.at.rstrip("/"))
  79. def read_text(self, *args, **kwargs):
  80. with self.open() as strm:
  81. return io.TextIOWrapper(strm, *args, **kwargs).read()
  82. def read_bytes(self):
  83. with self.open() as strm:
  84. return strm.read()
  85. def _is_child(self, path):
  86. return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
  87. def _next(self, at):
  88. return Path(self.root, at)
  89. def is_dir(self):
  90. return not self.at or self.at.endswith("/")
  91. def is_file(self):
  92. return not self.is_dir()
  93. def exists(self):
  94. return self.at in self._names()
  95. def iterdir(self):
  96. if not self.is_dir():
  97. raise ValueError("Can't listdir a file")
  98. subs = map(self._next, self._names())
  99. return filter(self._is_child, subs)
  100. def __str__(self):
  101. return posixpath.join(self.root.filename, self.at)
  102. def __repr__(self):
  103. return self.__repr.format(self=self)
  104. def joinpath(self, add):
  105. add = self._pathlib_compat(add)
  106. next = posixpath.join(self.at, add)
  107. next_dir = posixpath.join(self.at, add, "")
  108. names = self._names()
  109. return self._next(next_dir if next not in names and next_dir in names else next)
  110. __truediv__ = joinpath
  111. @staticmethod
  112. def _add_implied_dirs(names):
  113. return names + [
  114. name + "/"
  115. for name in map(posixpath.dirname, names)
  116. if name and name + "/" not in names
  117. ]
  118. @property
  119. def parent(self):
  120. parent_at = posixpath.dirname(self.at)
  121. if parent_at:
  122. parent_at += '/'
  123. return self._next(parent_at)
  124. def _names(self):
  125. return self._add_implied_dirs(self.root.namelist())
  126. if sys.version_info < (3,):
  127. __div__ = __truediv__