Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/zipp/__init__.py: 58%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2A Path-like interface for zipfiles.
4This codebase is shared between zipfile.Path in the stdlib
5and zipp in PyPI. See
6https://github.com/python/importlib_metadata/wiki/Development-Methodology
7for more detail.
8"""
10import functools
11import io
12import itertools
13import pathlib
14import posixpath
15import re
16import stat
17import zipfile
19from ._functools import none_as, save_method_args
20from .glob import Translator
22__all__ = ['Path']
25def _parents(path):
26 """
27 Given a path with elements separated by
28 posixpath.sep, generate all parents of that path.
30 >>> list(_parents('b/d'))
31 ['b']
32 >>> list(_parents('/b/d/'))
33 ['/b']
34 >>> list(_parents('b/d/f/'))
35 ['b/d', 'b']
36 >>> list(_parents('b'))
37 []
38 >>> list(_parents(''))
39 []
40 """
41 return itertools.islice(_ancestry(path), 1, None)
44def _ancestry(path):
45 """
46 Given a path with elements separated by
47 posixpath.sep, generate all elements of that path.
49 >>> list(_ancestry('b/d'))
50 ['b/d', 'b']
51 >>> list(_ancestry('/b/d/'))
52 ['/b/d', '/b']
53 >>> list(_ancestry('b/d/f/'))
54 ['b/d/f', 'b/d', 'b']
55 >>> list(_ancestry('b'))
56 ['b']
57 >>> list(_ancestry(''))
58 []
60 Multiple separators are treated like a single.
62 >>> list(_ancestry('//b//d///f//'))
63 ['//b//d///f', '//b//d', '//b']
64 """
65 path = path.rstrip(posixpath.sep)
66 while path.rstrip(posixpath.sep):
67 yield path
68 path, _tail = posixpath.split(path)
71_dedupe = dict.fromkeys
72"""Deduplicate an iterable in original order"""
75def _difference(minuend, subtrahend):
76 """
77 Return items in minuend not in subtrahend, retaining order
78 with O(1) lookup.
79 """
80 return itertools.filterfalse(set(subtrahend).__contains__, minuend)
83class InitializedState:
84 """
85 Mix-in to save the initialization state for pickling.
86 """
88 @save_method_args
89 def __init__(self, *args, **kwargs):
90 super().__init__(*args, **kwargs)
92 def __getstate__(self):
93 return self._saved___init__.args, self._saved___init__.kwargs
95 def __setstate__(self, state):
96 args, kwargs = state
97 super().__init__(*args, **kwargs)
100class CompleteDirs(InitializedState, zipfile.ZipFile):
101 """
102 A ZipFile subclass that ensures that implied directories
103 are always included in the namelist.
105 >>> list(CompleteDirs._implied_dirs(['foo/bar.txt', 'foo/bar/baz.txt']))
106 ['foo/', 'foo/bar/']
107 >>> list(CompleteDirs._implied_dirs(['foo/bar.txt', 'foo/bar/baz.txt', 'foo/bar/']))
108 ['foo/']
109 """
111 @staticmethod
112 def _implied_dirs(names):
113 parents = itertools.chain.from_iterable(map(_parents, names))
114 as_dirs = (p + posixpath.sep for p in parents)
115 return _dedupe(_difference(as_dirs, names))
117 def namelist(self):
118 names = super().namelist()
119 return names + list(self._implied_dirs(names))
121 def _name_set(self):
122 return set(self.namelist())
124 def resolve_dir(self, name):
125 """
126 If the name represents a directory, return that name
127 as a directory (with the trailing slash).
128 """
129 names = self._name_set()
130 dirname = name + '/'
131 dir_match = name not in names and dirname in names
132 return dirname if dir_match else name
134 def getinfo(self, name):
135 """
136 Supplement getinfo for implied dirs.
137 """
138 try:
139 return super().getinfo(name)
140 except KeyError:
141 if not name.endswith('/') or name not in self._name_set():
142 raise
143 return zipfile.ZipInfo(filename=name)
145 @classmethod
146 def make(cls, source):
147 """
148 Given a source (filename or zipfile), return an
149 appropriate CompleteDirs subclass.
150 """
151 if isinstance(source, CompleteDirs):
152 return source
154 if not isinstance(source, zipfile.ZipFile):
155 return cls(source)
157 # Only allow for FastLookup when supplied zipfile is read-only
158 source.__class__ = cls if 'r' in source.mode else CompleteDirs
159 return source
161 @classmethod
162 def inject(cls, zf: zipfile.ZipFile) -> zipfile.ZipFile:
163 """
164 Given a writable zip file zf, inject directory entries for
165 any directories implied by the presence of children.
166 """
167 for name in cls._implied_dirs(zf.namelist()):
168 zf.writestr(name, b"")
169 return zf
172class FastLookup(CompleteDirs):
173 """
174 ZipFile subclass to ensure implicit
175 dirs exist and are resolved rapidly.
176 """
178 def namelist(self):
179 return self._namelist
181 @functools.cached_property
182 def _namelist(self):
183 return super().namelist()
185 def _name_set(self):
186 return self._name_set_prop
188 @functools.cached_property
189 def _name_set_prop(self):
190 return super()._name_set()
193def _extract_text_encoding(encoding=None, *args, **kwargs):
194 stack_level = 3
195 return io.text_encoding(encoding, stack_level), args, kwargs
198class Path:
199 """
200 A :class:`importlib.resources.abc.Traversable` interface for zip files.
202 Implements many of the features users enjoy from
203 :class:`pathlib.Path`.
205 Consider a zip file with this structure::
207 .
208 ├── a.txt
209 └── b
210 ├── c.txt
211 └── d
212 └── e.txt
214 >>> data = io.BytesIO()
215 >>> zf = zipfile.ZipFile(data, 'w')
216 >>> zf.writestr('a.txt', 'content of a')
217 >>> zf.writestr('b/c.txt', 'content of c')
218 >>> zf.writestr('b/d/e.txt', 'content of e')
219 >>> zf.filename = 'mem/abcde.zip'
221 Path accepts the zipfile object itself or a filename
223 >>> path = Path(zf)
225 From there, several path operations are available.
227 Directory iteration (including the zip file itself):
229 >>> a, b = path.iterdir()
230 >>> a
231 Path('mem/abcde.zip', 'a.txt')
232 >>> b
233 Path('mem/abcde.zip', 'b/')
235 name property:
237 >>> b.name
238 'b'
240 join with divide operator:
242 >>> c = b / 'c.txt'
243 >>> c
244 Path('mem/abcde.zip', 'b/c.txt')
245 >>> c.name
246 'c.txt'
248 Read text:
250 >>> c.read_text(encoding='utf-8')
251 'content of c'
253 existence:
255 >>> c.exists()
256 True
257 >>> (b / 'missing.txt').exists()
258 False
260 Coercion to string:
262 >>> import os
263 >>> str(c).replace(os.sep, posixpath.sep)
264 'mem/abcde.zip/b/c.txt'
266 At the root, ``name``, ``filename``, and ``parent``
267 resolve to the zipfile.
269 >>> str(path)
270 'mem/abcde.zip'
271 >>> path.name
272 'abcde.zip'
273 >>> path.filename == pathlib.Path('mem/abcde.zip')
274 True
275 >>> str(path.parent)
276 'mem'
278 If the zipfile has no filename, such attributes are not
279 valid and accessing them will raise an Exception.
281 >>> zf.filename = None
282 >>> path.name
283 Traceback (most recent call last):
284 ...
285 TypeError: ...
287 >>> path.filename
288 Traceback (most recent call last):
289 ...
290 TypeError: ...
292 >>> path.parent
293 Traceback (most recent call last):
294 ...
295 TypeError: ...
297 # workaround python/cpython#106763
298 >>> pass
299 """
301 __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
303 def __init__(self, root, at=""):
304 """
305 Construct a Path from a ZipFile or filename.
307 Note: When the source is an existing ZipFile object,
308 its type (__class__) will be mutated to a
309 specialized type. If the caller wishes to retain the
310 original type, the caller should either create a
311 separate ZipFile object or pass a filename.
312 """
313 self.root = FastLookup.make(root)
314 self.at = at
316 def __eq__(self, other):
317 """
318 >>> Path(zipfile.ZipFile(io.BytesIO(), 'w')) == 'foo'
319 False
320 """
321 if self.__class__ is not other.__class__:
322 return NotImplemented
323 return (self.root, self.at) == (other.root, other.at)
325 def __hash__(self):
326 return hash((self.root, self.at))
328 def open(self, mode='r', *args, pwd=None, **kwargs):
329 """
330 Open this entry as text or binary following the semantics
331 of ``pathlib.Path.open()`` by passing arguments through
332 to io.TextIOWrapper().
333 """
334 if self.is_dir():
335 raise IsADirectoryError(self)
336 zip_mode = mode[0]
337 if zip_mode == 'r' and not self.exists():
338 raise FileNotFoundError(self)
339 stream = self.root.open(self.at, zip_mode, pwd=pwd)
340 if 'b' in mode:
341 if args or kwargs:
342 raise ValueError("encoding args invalid for binary operation")
343 return stream
344 # Text mode:
345 encoding, args, kwargs = _extract_text_encoding(*args, **kwargs)
346 return io.TextIOWrapper(stream, encoding, *args, **kwargs)
348 def _base(self):
349 return pathlib.PurePosixPath(self.at) if self.at else self.filename
351 @property
352 def name(self):
353 return self._base().name
355 @property
356 def suffix(self):
357 return self._base().suffix
359 @property
360 def suffixes(self):
361 return self._base().suffixes
363 @property
364 def stem(self):
365 return self._base().stem
367 @property
368 def filename(self):
369 return pathlib.Path(self.root.filename).joinpath(self.at)
371 def read_text(self, *args, **kwargs):
372 encoding, args, kwargs = _extract_text_encoding(*args, **kwargs)
373 with self.open('r', encoding, *args, **kwargs) as strm:
374 return strm.read()
376 def read_bytes(self):
377 with self.open('rb') as strm:
378 return strm.read()
380 def _is_child(self, path):
381 return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
383 def _next(self, at):
384 return self.__class__(self.root, at)
386 def is_dir(self):
387 return not self.at or self.at.endswith("/")
389 def is_file(self):
390 return self.exists() and not self.is_dir()
392 def exists(self):
393 return self.at in self.root._name_set()
395 def iterdir(self):
396 if not self.is_dir():
397 raise NotADirectoryError("Can't listdir a file")
398 subs = map(self._next, self.root.namelist())
399 return filter(self._is_child, subs)
401 def match(self, path_pattern):
402 return pathlib.PurePosixPath(self.at).match(path_pattern)
404 def is_symlink(self):
405 """
406 Return whether this path is a symlink.
407 """
408 info = self.root.getinfo(self.at)
409 mode = info.external_attr >> 16
410 return stat.S_ISLNK(mode)
412 def glob(self, pattern):
413 if not pattern:
414 raise ValueError(f"Unacceptable pattern: {pattern!r}")
416 prefix = re.escape(self.at)
417 tr = Translator(seps='/')
418 matches = re.compile(prefix + tr.translate(pattern)).fullmatch
419 return map(self._next, filter(matches, self.root.namelist()))
421 def rglob(self, pattern):
422 return self.glob(f'**/{pattern}')
424 def relative_to(self, other, *extra):
425 return posixpath.relpath(str(self), str(other.joinpath(*extra)))
427 def __str__(self):
428 root = none_as(self.root.filename, ':zipfile:')
429 return posixpath.join(root, self.at) if self.at else root
431 def __repr__(self):
432 return self.__repr.format(self=self)
434 def joinpath(self, *other):
435 next = posixpath.join(self.at, *other)
436 return self._next(self.root.resolve_dir(next))
438 __truediv__ = joinpath
440 @property
441 def parent(self):
442 if not self.at:
443 return self.filename.parent
444 parent_at = posixpath.dirname(self.at.rstrip('/'))
445 if parent_at:
446 parent_at += '/'
447 return self._next(parent_at)