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

179 statements  

1""" 

2A Path-like interface for zipfiles. 

3 

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""" 

9 

10import functools 

11import io 

12import itertools 

13import pathlib 

14import posixpath 

15import re 

16import stat 

17import zipfile 

18 

19from ._functools import none_as, save_method_args 

20from .glob import Translator 

21 

22__all__ = ['Path'] 

23 

24 

25def _parents(path): 

26 """ 

27 Given a path with elements separated by 

28 posixpath.sep, generate all parents of that path. 

29 

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) 

42 

43 

44def _ancestry(path): 

45 """ 

46 Given a path with elements separated by 

47 posixpath.sep, generate all elements of that path. 

48 

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 [] 

59 

60 Multiple separators are treated like a single. 

61 

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) 

69 

70 

71_dedupe = dict.fromkeys 

72"""Deduplicate an iterable in original order""" 

73 

74 

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) 

81 

82 

83class InitializedState: 

84 """ 

85 Mix-in to save the initialization state for pickling. 

86 """ 

87 

88 @save_method_args 

89 def __init__(self, *args, **kwargs): 

90 super().__init__(*args, **kwargs) 

91 

92 def __getstate__(self): 

93 return self._saved___init__.args, self._saved___init__.kwargs 

94 

95 def __setstate__(self, state): 

96 args, kwargs = state 

97 super().__init__(*args, **kwargs) 

98 

99 

100class CompleteDirs(InitializedState, zipfile.ZipFile): 

101 """ 

102 A ZipFile subclass that ensures that implied directories 

103 are always included in the namelist. 

104 

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 """ 

110 

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)) 

116 

117 def namelist(self): 

118 names = super().namelist() 

119 return names + list(self._implied_dirs(names)) 

120 

121 def _name_set(self): 

122 return set(self.namelist()) 

123 

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 

133 

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) 

144 

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 

153 

154 if not isinstance(source, zipfile.ZipFile): 

155 return cls(source) 

156 

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 

160 

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 

170 

171 

172class FastLookup(CompleteDirs): 

173 """ 

174 ZipFile subclass to ensure implicit 

175 dirs exist and are resolved rapidly. 

176 """ 

177 

178 def namelist(self): 

179 return self._namelist 

180 

181 @functools.cached_property 

182 def _namelist(self): 

183 return super().namelist() 

184 

185 def _name_set(self): 

186 return self._name_set_prop 

187 

188 @functools.cached_property 

189 def _name_set_prop(self): 

190 return super()._name_set() 

191 

192 

193def _extract_text_encoding(encoding=None, *args, **kwargs): 

194 stack_level = 3 

195 return io.text_encoding(encoding, stack_level), args, kwargs 

196 

197 

198class Path: 

199 """ 

200 A :class:`importlib.resources.abc.Traversable` interface for zip files. 

201 

202 Implements many of the features users enjoy from 

203 :class:`pathlib.Path`. 

204 

205 Consider a zip file with this structure:: 

206 

207 . 

208 ├── a.txt 

209 └── b 

210 ├── c.txt 

211 └── d 

212 └── e.txt 

213 

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' 

220 

221 Path accepts the zipfile object itself or a filename 

222 

223 >>> path = Path(zf) 

224 

225 From there, several path operations are available. 

226 

227 Directory iteration (including the zip file itself): 

228 

229 >>> a, b = path.iterdir() 

230 >>> a 

231 Path('mem/abcde.zip', 'a.txt') 

232 >>> b 

233 Path('mem/abcde.zip', 'b/') 

234 

235 name property: 

236 

237 >>> b.name 

238 'b' 

239 

240 join with divide operator: 

241 

242 >>> c = b / 'c.txt' 

243 >>> c 

244 Path('mem/abcde.zip', 'b/c.txt') 

245 >>> c.name 

246 'c.txt' 

247 

248 Read text: 

249 

250 >>> c.read_text(encoding='utf-8') 

251 'content of c' 

252 

253 existence: 

254 

255 >>> c.exists() 

256 True 

257 >>> (b / 'missing.txt').exists() 

258 False 

259 

260 Coercion to string: 

261 

262 >>> import os 

263 >>> str(c).replace(os.sep, posixpath.sep) 

264 'mem/abcde.zip/b/c.txt' 

265 

266 At the root, ``name``, ``filename``, and ``parent`` 

267 resolve to the zipfile. 

268 

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' 

277 

278 If the zipfile has no filename, such attributes are not 

279 valid and accessing them will raise an Exception. 

280 

281 >>> zf.filename = None 

282 >>> path.name 

283 Traceback (most recent call last): 

284 ... 

285 TypeError: ... 

286 

287 >>> path.filename 

288 Traceback (most recent call last): 

289 ... 

290 TypeError: ... 

291 

292 >>> path.parent 

293 Traceback (most recent call last): 

294 ... 

295 TypeError: ... 

296 

297 # workaround python/cpython#106763 

298 >>> pass 

299 """ 

300 

301 __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})" 

302 

303 def __init__(self, root, at=""): 

304 """ 

305 Construct a Path from a ZipFile or filename. 

306 

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 

315 

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) 

324 

325 def __hash__(self): 

326 return hash((self.root, self.at)) 

327 

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) 

347 

348 def _base(self): 

349 return pathlib.PurePosixPath(self.at) if self.at else self.filename 

350 

351 @property 

352 def name(self): 

353 return self._base().name 

354 

355 @property 

356 def suffix(self): 

357 return self._base().suffix 

358 

359 @property 

360 def suffixes(self): 

361 return self._base().suffixes 

362 

363 @property 

364 def stem(self): 

365 return self._base().stem 

366 

367 @property 

368 def filename(self): 

369 return pathlib.Path(self.root.filename).joinpath(self.at) 

370 

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() 

375 

376 def read_bytes(self): 

377 with self.open('rb') as strm: 

378 return strm.read() 

379 

380 def _is_child(self, path): 

381 return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/") 

382 

383 def _next(self, at): 

384 return self.__class__(self.root, at) 

385 

386 def is_dir(self): 

387 return not self.at or self.at.endswith("/") 

388 

389 def is_file(self): 

390 return self.exists() and not self.is_dir() 

391 

392 def exists(self): 

393 return self.at in self.root._name_set() 

394 

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) 

400 

401 def match(self, path_pattern): 

402 return pathlib.PurePosixPath(self.at).match(path_pattern) 

403 

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) 

411 

412 def glob(self, pattern): 

413 if not pattern: 

414 raise ValueError(f"Unacceptable pattern: {pattern!r}") 

415 

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())) 

420 

421 def rglob(self, pattern): 

422 return self.glob(f'**/{pattern}') 

423 

424 def relative_to(self, other, *extra): 

425 return posixpath.relpath(str(self), str(other.joinpath(*extra))) 

426 

427 def __str__(self): 

428 root = none_as(self.root.filename, ':zipfile:') 

429 return posixpath.join(root, self.at) if self.at else root 

430 

431 def __repr__(self): 

432 return self.__repr.format(self=self) 

433 

434 def joinpath(self, *other): 

435 next = posixpath.join(self.at, *other) 

436 return self._next(self.root.resolve_dir(next)) 

437 

438 __truediv__ = joinpath 

439 

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)