You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
4.9 KiB

4 years ago
  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. import itertools
  9. import more_itertools
  10. __metaclass__ = type
  11. def _parents(path):
  12. """
  13. Given a path with elements separated by
  14. posixpath.sep, generate all parents of that path.
  15. >>> list(_parents('b/d'))
  16. ['b']
  17. >>> list(_parents('/b/d/'))
  18. ['/b']
  19. >>> list(_parents('b/d/f/'))
  20. ['b/d', 'b']
  21. >>> list(_parents('b'))
  22. []
  23. >>> list(_parents(''))
  24. []
  25. """
  26. return itertools.islice(_ancestry(path), 1, None)
  27. def _ancestry(path):
  28. """
  29. Given a path with elements separated by
  30. posixpath.sep, generate all elements of that path
  31. >>> list(_ancestry('b/d'))
  32. ['b/d', 'b']
  33. >>> list(_ancestry('/b/d/'))
  34. ['/b/d', '/b']
  35. >>> list(_ancestry('b/d/f/'))
  36. ['b/d/f', 'b/d', 'b']
  37. >>> list(_ancestry('b'))
  38. ['b']
  39. >>> list(_ancestry(''))
  40. []
  41. """
  42. path = path.rstrip(posixpath.sep)
  43. while path and path != posixpath.sep:
  44. yield path
  45. path, tail = posixpath.split(path)
  46. class Path:
  47. """
  48. A pathlib-compatible interface for zip files.
  49. Consider a zip file with this structure::
  50. .
  51. a.txt
  52. b
  53. c.txt
  54. d
  55. e.txt
  56. >>> data = io.BytesIO()
  57. >>> zf = zipfile.ZipFile(data, 'w')
  58. >>> zf.writestr('a.txt', 'content of a')
  59. >>> zf.writestr('b/c.txt', 'content of c')
  60. >>> zf.writestr('b/d/e.txt', 'content of e')
  61. >>> zf.filename = 'abcde.zip'
  62. Path accepts the zipfile object itself or a filename
  63. >>> root = Path(zf)
  64. From there, several path operations are available.
  65. Directory iteration (including the zip file itself):
  66. >>> a, b = root.iterdir()
  67. >>> a
  68. Path('abcde.zip', 'a.txt')
  69. >>> b
  70. Path('abcde.zip', 'b/')
  71. name property:
  72. >>> b.name
  73. 'b'
  74. join with divide operator:
  75. >>> c = b / 'c.txt'
  76. >>> c
  77. Path('abcde.zip', 'b/c.txt')
  78. >>> c.name
  79. 'c.txt'
  80. Read text:
  81. >>> c.read_text()
  82. 'content of c'
  83. existence:
  84. >>> c.exists()
  85. True
  86. >>> (b / 'missing.txt').exists()
  87. False
  88. Coercion to string:
  89. >>> str(c)
  90. 'abcde.zip/b/c.txt'
  91. """
  92. __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
  93. def __init__(self, root, at=""):
  94. self.root = (
  95. root
  96. if isinstance(root, zipfile.ZipFile)
  97. else zipfile.ZipFile(self._pathlib_compat(root))
  98. )
  99. self.at = at
  100. @staticmethod
  101. def _pathlib_compat(path):
  102. """
  103. For path-like objects, convert to a filename for compatibility
  104. on Python 3.6.1 and earlier.
  105. """
  106. try:
  107. return path.__fspath__()
  108. except AttributeError:
  109. return str(path)
  110. @property
  111. def open(self):
  112. return functools.partial(self.root.open, self.at)
  113. @property
  114. def name(self):
  115. return posixpath.basename(self.at.rstrip("/"))
  116. def read_text(self, *args, **kwargs):
  117. with self.open() as strm:
  118. return io.TextIOWrapper(strm, *args, **kwargs).read()
  119. def read_bytes(self):
  120. with self.open() as strm:
  121. return strm.read()
  122. def _is_child(self, path):
  123. return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
  124. def _next(self, at):
  125. return Path(self.root, at)
  126. def is_dir(self):
  127. return not self.at or self.at.endswith("/")
  128. def is_file(self):
  129. return not self.is_dir()
  130. def exists(self):
  131. return self.at in self._names()
  132. def iterdir(self):
  133. if not self.is_dir():
  134. raise ValueError("Can't listdir a file")
  135. subs = map(self._next, self._names())
  136. return filter(self._is_child, subs)
  137. def __str__(self):
  138. return posixpath.join(self.root.filename, self.at)
  139. def __repr__(self):
  140. return self.__repr.format(self=self)
  141. def joinpath(self, add):
  142. add = self._pathlib_compat(add)
  143. next = posixpath.join(self.at, add)
  144. next_dir = posixpath.join(self.at, add, "")
  145. names = self._names()
  146. return self._next(next_dir if next not in names and next_dir in names else next)
  147. __truediv__ = joinpath
  148. @staticmethod
  149. def _implied_dirs(names):
  150. return more_itertools.unique_everseen(
  151. parent + "/"
  152. for name in names
  153. for parent in _parents(name)
  154. if parent + "/" not in names
  155. )
  156. @classmethod
  157. def _add_implied_dirs(cls, names):
  158. return names + list(cls._implied_dirs(names))
  159. @property
  160. def parent(self):
  161. parent_at = posixpath.dirname(self.at.rstrip('/'))
  162. if parent_at:
  163. parent_at += '/'
  164. return self._next(parent_at)
  165. def _names(self):
  166. return self._add_implied_dirs(self.root.namelist())
  167. if sys.version_info < (3,):
  168. __div__ = __truediv__