Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fsspec/spec.py: 25%

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

992 statements  

1from __future__ import annotations 

2 

3import io 

4import json 

5import logging 

6import os 

7import threading 

8import warnings 

9import weakref 

10from errno import ESPIPE 

11from glob import has_magic 

12from hashlib import sha256 

13from typing import Any 

14 

15from .callbacks import DEFAULT_CALLBACK 

16from .config import apply_config, conf 

17from .dircache import DirCache 

18from .transaction import Transaction 

19from .utils import ( 

20 _unstrip_protocol, 

21 check_contained, 

22 glob_translate, 

23 isfilelike, 

24 other_paths, 

25 read_block, 

26 stringify_path, 

27 tokenize, 

28) 

29 

30logger = logging.getLogger("fsspec") 

31 

32 

33def make_instance(cls, args, kwargs): 

34 return cls(*args, **kwargs) 

35 

36 

37FORK_AVAILABLE = hasattr(os, "register_at_fork") 

38 

39 

40if FORK_AVAILABLE: 

41 _registered_classes = weakref.WeakSet() 

42 

43 def _reset_instances_lock(): 

44 for cls in _registered_classes: 

45 cls._instantiation_lock = threading.RLock() 

46 cls._token_locks = {} 

47 cls._cache.clear() 

48 cls._pid = os.getpid() 

49 

50 os.register_at_fork(after_in_child=_reset_instances_lock) 

51 

52 

53class _TokenLock: 

54 def __init__(self): 

55 self.lock = threading.RLock() 

56 self.error = None # set if creating the instance failed 

57 

58 

59class _Cached(type): 

60 """ 

61 Metaclass for caching file system instances. 

62 

63 Notes 

64 ----- 

65 Instances are cached according to 

66 

67 * The values of the class attributes listed in `_extra_tokenize_attributes` 

68 * The arguments passed to ``__init__``. 

69 

70 This creates an additional reference to the filesystem, which prevents the 

71 filesystem from being garbage collected when all *user* references go away. 

72 A call to the :meth:`AbstractFileSystem.clear_instance_cache` must *also* 

73 be made for a filesystem instance to be garbage collected. 

74 """ 

75 

76 def __init__(cls, *args, **kwargs): 

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

78 

79 # Note: we intentionally create a reference here, to avoid garbage 

80 # collecting instances when all other references are gone. To really 

81 # delete a FileSystem, the cache must be cleared. 

82 if conf.get("weakref_instance_cache"): # pragma: no cover 

83 # debug option for analysing fork/spawn conditions 

84 cls._cache = weakref.WeakValueDictionary() 

85 else: 

86 cls._cache = {} 

87 cls._pid = os.getpid() 

88 cls._instantiation_lock = threading.RLock() 

89 # locks of the tokens whose instance is being created, see __call__ 

90 cls._token_locks = {} 

91 

92 if FORK_AVAILABLE: 

93 _registered_classes.add(cls) 

94 

95 def _check_instance_cache(cls, token): 

96 inst = cls._cache.get(token) 

97 if inst is not None: 

98 cls._latest = token 

99 return inst 

100 

101 def __call__(cls, *args, **kwargs): 

102 kwargs = apply_config(cls, kwargs) 

103 extra_tokens = tuple( 

104 getattr(cls, attr, None) for attr in cls._extra_tokenize_attributes 

105 ) 

106 strip_tokenize_options = { 

107 k: kwargs.pop(k) for k in cls._strip_tokenize_options if k in kwargs 

108 } 

109 pid = os.getpid() 

110 

111 if getattr(cls, "async_impl", False) and not kwargs.get("asynchronous", False): 

112 token = tokenize(cls, pid, *args, *extra_tokens, **kwargs) 

113 else: 

114 token = tokenize( 

115 cls, pid, threading.get_ident(), *args, *extra_tokens, **kwargs 

116 ) 

117 skip = kwargs.pop("skip_instance_cache", False) 

118 

119 if pid != cls._pid: 

120 with cls._instantiation_lock: 

121 if pid != cls._pid: 

122 cls._cache.clear() 

123 cls._token_locks.clear() 

124 cls._pid = pid 

125 

126 if not skip and cls.cachable: 

127 inst = cls._check_instance_cache(token) 

128 if inst is not None: 

129 return inst 

130 

131 with cls._instantiation_lock: 

132 # protect against the race condition that a new instance was created 

133 # and inserted into the cache since the initial check just above 

134 inst = cls._check_instance_cache(token) 

135 if inst is not None: 

136 return inst 

137 # Only one thread creates the instance for a given token, the others 

138 # wait for it instead of creating (and discarding) their own, since 

139 # creating an instance can be expensive (e.g. looking up credentials). 

140 # Instances for different tokens are still created concurrently. 

141 # This is under _instantiation_lock, so all threads get the same lock. 

142 token_lock = cls._token_locks.setdefault(token, _TokenLock()) 

143 

144 with token_lock.lock: 

145 if token_lock.error is not None: 

146 # Fail like the thread we waited for instead of retrying: with 

147 # the same arguments it most likely fails the same way, and the 

148 # last waiter would fail only after all the others retried. 

149 raise token_lock.error 

150 inst = cls._check_instance_cache(token) 

151 if inst is not None: 

152 return inst 

153 try: 

154 return cls.__create_instance( 

155 token, args, kwargs, strip_tokenize_options, cache=True 

156 ) 

157 except Exception as e: 

158 token_lock.error = e 

159 raise 

160 finally: 

161 # removed even on failure, so that later calls try again 

162 with cls._instantiation_lock: 

163 cls._token_locks.pop(token, None) 

164 

165 return cls.__create_instance( 

166 token, args, kwargs, strip_tokenize_options, cache=False 

167 ) 

168 

169 def __create_instance(cls, token, args, kwargs, strip_tokenize_options, cache): 

170 obj = super().__call__(*args, **kwargs, **strip_tokenize_options) 

171 # Setting _fs_token here causes some static linters to complain. 

172 obj._fs_token_ = token 

173 obj.storage_args = args 

174 obj.storage_options = kwargs 

175 if obj.async_impl and obj.mirror_sync_methods: 

176 from .asyn import mirror_sync_methods 

177 

178 mirror_sync_methods(obj) 

179 

180 if cache: 

181 with cls._instantiation_lock: 

182 # another thread may have created the instance while we were calling 

183 # super().__call__(), so we check again. 

184 inst = cls._check_instance_cache(token) 

185 if inst is not None: 

186 return inst 

187 

188 cls._latest = token 

189 cls._cache[token] = obj 

190 return obj 

191 

192 

193class AbstractFileSystem(metaclass=_Cached): 

194 """ 

195 An abstract super-class for pythonic file-systems 

196 

197 Implementations are expected to be compatible with or, better, subclass 

198 from here. 

199 """ 

200 

201 cachable = True # this class can be cached, instances reused 

202 _cached = False 

203 blocksize = 2**22 

204 sep = "/" 

205 # Implementations may select their protocol per instance (for example, a 

206 # single adapter class backed by different storage implementations). 

207 protocol: str | tuple[str, ...] = "abstract" 

208 _latest = None 

209 async_impl = False 

210 mirror_sync_methods = False 

211 root_marker = "" # For some FSs, may require leading '/' or other character 

212 transaction_type = Transaction 

213 

214 #: Extra *class attributes* that should be considered when hashing. 

215 _extra_tokenize_attributes = () 

216 #: *storage options* that should not be considered when hashing. 

217 _strip_tokenize_options = () 

218 

219 # Set by _Cached metaclass 

220 storage_args: tuple[Any, ...] 

221 storage_options: dict[str, Any] 

222 

223 def __init__(self, *args, **storage_options): 

224 """Create and configure file-system instance 

225 

226 Instances may be cachable, so if similar enough arguments are seen 

227 a new instance is not required. The token attribute exists to allow 

228 implementations to cache instances if they wish. 

229 

230 A reasonable default should be provided if there are no arguments. 

231 

232 Subclasses should call this method. 

233 

234 Parameters 

235 ---------- 

236 use_listings_cache, listings_expiry_time, max_paths: 

237 passed to ``DirCache``, if the implementation supports 

238 directory listing caching. Pass use_listings_cache=False 

239 to disable such caching. 

240 skip_instance_cache: bool 

241 If this is a cachable implementation, pass True here to force 

242 creating a new instance even if a matching instance exists, and prevent 

243 storing this instance. 

244 asynchronous: bool 

245 loop: asyncio-compatible IOLoop or None 

246 """ 

247 if self._cached: 

248 # reusing instance, don't change 

249 return 

250 self._cached = True 

251 self._intrans = False 

252 self._transaction = None 

253 self._invalidated_caches_in_transaction = [] 

254 self.dircache = DirCache(**storage_options) 

255 

256 if storage_options.pop("add_docs", None): 

257 warnings.warn("add_docs is no longer supported.", FutureWarning) 

258 

259 if storage_options.pop("add_aliases", None): 

260 warnings.warn("add_aliases has been removed.", FutureWarning) 

261 # This is set in _Cached 

262 self._fs_token_ = None 

263 

264 @property 

265 def fsid(self): 

266 """Persistent filesystem id that can be used to compare filesystems 

267 across sessions. 

268 """ 

269 raise NotImplementedError 

270 

271 @property 

272 def _fs_token(self): 

273 return self._fs_token_ 

274 

275 def __dask_tokenize__(self): 

276 return self._fs_token 

277 

278 def __hash__(self): 

279 return int(self._fs_token, 16) 

280 

281 def __eq__(self, other): 

282 return isinstance(other, type(self)) and self._fs_token == other._fs_token 

283 

284 def __reduce__(self): 

285 return make_instance, (type(self), self.storage_args, self.storage_options) 

286 

287 @classmethod 

288 def _strip_protocol(cls, path): 

289 """Turn path from fully-qualified to file-system-specific 

290 

291 May require FS-specific handling, e.g., for relative paths or links. 

292 """ 

293 if isinstance(path, list): 

294 return [cls._strip_protocol(p) for p in path] 

295 path = stringify_path(path) 

296 protos = (cls.protocol,) if isinstance(cls.protocol, str) else cls.protocol 

297 for protocol in protos: 

298 if path.startswith(protocol + "://"): 

299 path = path[len(protocol) + 3 :] 

300 elif path.startswith(protocol + "::"): 

301 path = path[len(protocol) + 2 :] 

302 path = path.rstrip("/") 

303 # use of root_marker to make minimum required path, e.g., "/" 

304 return path or cls.root_marker 

305 

306 def unstrip_protocol(self, name: str) -> str: 

307 """Format FS-specific path to generic, including protocol""" 

308 protos = (self.protocol,) if isinstance(self.protocol, str) else self.protocol 

309 for protocol in protos: 

310 if name.startswith(f"{protocol}://"): 

311 return name 

312 return f"{protos[0]}://{name}" 

313 

314 @staticmethod 

315 def _get_kwargs_from_urls(path): 

316 """If kwargs can be encoded in the paths, extract them here 

317 

318 This should happen before instantiation of the class; incoming paths 

319 then should be amended to strip the options in methods. 

320 

321 Examples may look like an sftp path "sftp://user@host:/my/path", where 

322 the user and host should become kwargs and later get stripped. 

323 """ 

324 # by default, nothing happens 

325 return {} 

326 

327 @classmethod 

328 def current(cls): 

329 """Return the most recently instantiated FileSystem 

330 

331 If no instance has been created, then create one with defaults 

332 """ 

333 inst = cls._cache.get(cls._latest) 

334 if inst is not None: 

335 return inst 

336 return cls() 

337 

338 @property 

339 def transaction(self): 

340 """A context within which files are committed together upon exit 

341 

342 Requires the file class to implement `.commit()` and `.discard()` 

343 for the normal and exception cases. 

344 """ 

345 if self._transaction is None: 

346 self._transaction = self.transaction_type(self) 

347 return self._transaction 

348 

349 def start_transaction(self): 

350 """Begin write transaction for deferring files, non-context version""" 

351 self._intrans = True 

352 self._transaction = self.transaction_type(self) 

353 return self.transaction 

354 

355 def end_transaction(self): 

356 """Finish write transaction, non-context version""" 

357 self.transaction.complete() 

358 self._transaction = None 

359 # The invalid cache must be cleared after the transaction is completed. 

360 for path in self._invalidated_caches_in_transaction: 

361 self.invalidate_cache(path) 

362 self._invalidated_caches_in_transaction.clear() 

363 

364 def invalidate_cache(self, path=None): 

365 """ 

366 Discard any cached directory information 

367 

368 Parameters 

369 ---------- 

370 path: string or None 

371 If None, clear all listings cached else listings at or under given 

372 path. 

373 """ 

374 # Not necessary to implement invalidation mechanism, may have no cache. 

375 # But if have, you should call this method of parent class from your 

376 # subclass to ensure expiring caches after transacations correctly. 

377 # See the implementation of FTPFileSystem in ftp.py 

378 if self._intrans: 

379 self._invalidated_caches_in_transaction.append(path) 

380 

381 def mkdir(self, path, create_parents=True, **kwargs): 

382 """ 

383 Create directory entry at path 

384 

385 For systems that don't have true directories, may create an for 

386 this instance only and not touch the real filesystem 

387 

388 Parameters 

389 ---------- 

390 path: str 

391 location 

392 create_parents: bool 

393 if True, this is equivalent to ``makedirs`` 

394 kwargs: 

395 may be permissions, etc. 

396 """ 

397 pass # not necessary to implement, may not have directories 

398 

399 def makedirs(self, path, exist_ok=False): 

400 """Recursively make directories 

401 

402 Creates directory at path and any intervening required directories. 

403 Raises exception if, for instance, the path already exists but is a 

404 file. 

405 

406 Parameters 

407 ---------- 

408 path: str 

409 leaf directory name 

410 exist_ok: bool (False) 

411 If False, will error if the target already exists 

412 """ 

413 pass # not necessary to implement, may not have directories 

414 

415 def rmdir(self, path): 

416 """Remove a directory, if empty""" 

417 pass # not necessary to implement, may not have directories 

418 

419 def ls(self, path, detail=True, **kwargs): 

420 """List objects at path. 

421 

422 This should include subdirectories and files at that location. The 

423 difference between a file and a directory must be clear when details 

424 are requested. 

425 

426 The specific keys, or perhaps a FileInfo class, or similar, is TBD, 

427 but must be consistent across implementations. 

428 Must include: 

429 

430 - full path to the entry (without protocol) 

431 - size of the entry, in bytes. If the value cannot be determined, will 

432 be ``None``. 

433 - type of entry, "file", "directory" or other 

434 

435 Additional information 

436 may be present, appropriate to the file-system, e.g., generation, 

437 checksum, etc. 

438 

439 May use refresh=True|False to allow use of self._ls_from_cache to 

440 check for a saved listing and avoid calling the backend. This would be 

441 common where listing may be expensive. 

442 

443 Parameters 

444 ---------- 

445 path: str 

446 detail: bool 

447 if True, gives a list of dictionaries, where each is the same as 

448 the result of ``info(path)``. If False, gives a list of paths 

449 (str). 

450 kwargs: may have additional backend-specific options, such as version 

451 information 

452 

453 Returns 

454 ------- 

455 List of strings if detail is False, or list of directory information 

456 dicts if detail is True. 

457 """ 

458 raise NotImplementedError 

459 

460 def _ls_from_cache(self, path): 

461 """Check cache for listing 

462 

463 Returns listing, if found (may be empty list for a directly that exists 

464 but contains nothing), None if not in cache. 

465 """ 

466 parent = self._parent(path) 

467 try: 

468 return self.dircache[path.rstrip("/")] 

469 except KeyError: 

470 pass 

471 try: 

472 files = [ 

473 f 

474 for f in self.dircache[parent] 

475 if f["name"] == path 

476 or (f["name"] == path.rstrip("/") and f["type"] == "directory") 

477 ] 

478 if len(files) == 0: 

479 # parent dir was listed but did not contain this file 

480 raise FileNotFoundError(path) 

481 return files 

482 except KeyError: 

483 pass 

484 

485 def walk(self, path, maxdepth=None, topdown=True, on_error="omit", **kwargs): 

486 """Return all files under the given path. 

487 

488 List all files, recursing into subdirectories; output is iterator-style, 

489 like ``os.walk()``. For a simple list of files, ``find()`` is available. 

490 

491 When topdown is True, the caller can modify the dirnames list in-place (perhaps 

492 using del or slice assignment), and walk() will 

493 only recurse into the subdirectories whose names remain in dirnames; 

494 this can be used to prune the search, impose a specific order of visiting, 

495 or even to inform walk() about directories the caller creates or renames before 

496 it resumes walk() again. 

497 Modifying dirnames when topdown is False has no effect. (see os.walk) 

498 

499 Note that the "files" outputted will include anything that is not 

500 a directory, such as links. 

501 

502 Parameters 

503 ---------- 

504 path: str 

505 Root to recurse into 

506 maxdepth: int 

507 Maximum recursion depth. None means limitless, but not recommended 

508 on link-based file-systems. 

509 topdown: bool (True) 

510 Whether to walk the directory tree from the top downwards or from 

511 the bottom upwards. 

512 on_error: "omit", "raise", a callable 

513 if omit (default), path with exception will simply be empty; 

514 If raise, an underlying exception will be raised; 

515 if callable, it will be called with a single OSError instance as argument 

516 kwargs: passed to ``ls`` 

517 """ 

518 if maxdepth is not None and maxdepth < 1: 

519 raise ValueError("maxdepth must be at least 1") 

520 

521 path = self._strip_protocol(path) 

522 full_dirs = {} 

523 dirs = {} 

524 files = {} 

525 

526 detail = kwargs.pop("detail", False) 

527 try: 

528 listing = self.ls(path, detail=True, **kwargs) 

529 except (FileNotFoundError, OSError) as e: 

530 if on_error == "raise": 

531 raise 

532 if callable(on_error): 

533 on_error(e) 

534 return 

535 

536 for info in listing: 

537 # each info name must be at least [path]/part , but here 

538 # we check also for names like [path]/part/ 

539 pathname = info["name"].rstrip("/") 

540 name = pathname.rsplit("/", 1)[-1] 

541 if info["type"] == "directory" and pathname != path: 

542 # do not include "self" path 

543 full_dirs[name] = pathname 

544 dirs[name] = info 

545 elif pathname == path: 

546 # file-like with same name as give path 

547 files[""] = info 

548 else: 

549 files[name] = info 

550 

551 if not detail: 

552 dirs = list(dirs) 

553 files = list(files) 

554 

555 if topdown: 

556 # Yield before recursion if walking top down 

557 yield path, dirs, files 

558 

559 if maxdepth is not None: 

560 maxdepth -= 1 

561 if maxdepth < 1: 

562 if not topdown: 

563 yield path, dirs, files 

564 return 

565 

566 for d in dirs: 

567 yield from self.walk( 

568 full_dirs[d], 

569 maxdepth=maxdepth, 

570 detail=detail, 

571 topdown=topdown, 

572 on_error=on_error, 

573 **kwargs, 

574 ) 

575 

576 if not topdown: 

577 # Yield after recursion if walking bottom up 

578 yield path, dirs, files 

579 

580 def find(self, path, maxdepth=None, withdirs=False, detail=False, **kwargs): 

581 """List all files below path. 

582 

583 Like posix ``find`` command without conditions 

584 

585 Parameters 

586 ---------- 

587 path : str 

588 maxdepth: int or None 

589 If not None, the maximum number of levels to descend 

590 withdirs: bool 

591 Whether to include directory paths in the output. This is True 

592 when used by glob, but users usually only want files. 

593 kwargs are passed to ``ls``. 

594 """ 

595 # TODO: allow equivalent of -name parameter 

596 path = self._strip_protocol(path) 

597 out = {} 

598 

599 # Add the root directory if withdirs is requested 

600 # This is needed for posix glob compliance 

601 if withdirs and path != "" and self.isdir(path): 

602 out[path] = self.info(path) 

603 

604 for _, dirs, files in self.walk(path, maxdepth, detail=True, **kwargs): 

605 if withdirs: 

606 files.update(dirs) 

607 out.update({info["name"]: info for name, info in files.items()}) 

608 if not out and self.isfile(path): 

609 # walk works on directories, but find should also return [path] 

610 # when path happens to be a file 

611 out[path] = {} 

612 names = sorted(out) 

613 if not detail: 

614 return names 

615 else: 

616 return {name: out[name] for name in names} 

617 

618 def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs): 

619 """Space used by files and optionally directories within a path 

620 

621 Directory size does not include the size of its contents. 

622 

623 Parameters 

624 ---------- 

625 path: str 

626 total: bool 

627 Whether to sum all the file sizes 

628 maxdepth: int or None 

629 Maximum number of directory levels to descend, None for unlimited. 

630 withdirs: bool 

631 Whether to include directory paths in the output. 

632 kwargs: passed to ``find`` 

633 

634 Returns 

635 ------- 

636 Dict of {path: size} if total=False, or int otherwise, where numbers 

637 refer to bytes used. 

638 """ 

639 sizes = {} 

640 if withdirs and self.isdir(path): 

641 # Include top-level directory in output 

642 info = self.info(path) 

643 sizes[info["name"]] = info["size"] 

644 for f in self.find(path, maxdepth=maxdepth, withdirs=withdirs, **kwargs): 

645 info = self.info(f) 

646 sizes[info["name"]] = info["size"] 

647 if total: 

648 return sum(sizes.values()) 

649 else: 

650 return sizes 

651 

652 def glob(self, path, maxdepth=None, **kwargs): 

653 """Find files by glob-matching. 

654 

655 Pattern matching capabilities for finding files that match the given pattern. 

656 

657 Parameters 

658 ---------- 

659 path: str 

660 The glob pattern to match against 

661 maxdepth: int or None 

662 Maximum depth for ``'**'`` patterns. Applied on the first ``'**'`` found. 

663 Must be at least 1 if provided. 

664 kwargs: 

665 Additional arguments passed to ``find`` (e.g., detail=True) 

666 

667 Returns 

668 ------- 

669 List of matched paths, or dict of paths and their info if detail=True 

670 

671 Notes 

672 ----- 

673 Supported patterns: 

674 - '*': Matches any sequence of characters within a single directory level 

675 - ``'**'``: Matches any number of directory levels (must be an entire path component) 

676 - '?': Matches exactly one character 

677 - '[abc]': Matches any character in the set 

678 - '[a-z]': Matches any character in the range 

679 - '[!abc]': Matches any character NOT in the set 

680 

681 Special behaviors: 

682 - If the path ends with '/', only folders are returned 

683 - Consecutive '*' characters are compressed into a single '*' 

684 - Empty set '[]' or negated empty negated set '[!]' never match anything 

685 - Special characters in character classes are escaped properly 

686 

687 Limitations: 

688 - ``'**'`` must be a complete path component (e.g., ``'a/**/b'``, not ``'a**b'``) 

689 - No brace expansion ('{a,b}.txt') 

690 - No extended glob patterns ('+(pattern)', '!(pattern)') 

691 """ 

692 if maxdepth is not None and maxdepth < 1: 

693 raise ValueError("maxdepth must be at least 1") 

694 

695 import re 

696 

697 seps = (os.path.sep, os.path.altsep) if os.path.altsep else (os.path.sep,) 

698 ends_with_sep = path.endswith(seps) # _strip_protocol strips trailing slash 

699 path = self._strip_protocol(path) 

700 append_slash_to_dirname = ends_with_sep or path.endswith( 

701 tuple(sep + "**" for sep in seps) 

702 ) 

703 idx_star = path.find("*") if path.find("*") >= 0 else len(path) 

704 idx_qmark = path.find("?") if path.find("?") >= 0 else len(path) 

705 idx_brace = path.find("[") if path.find("[") >= 0 else len(path) 

706 

707 min_idx = min(idx_star, idx_qmark, idx_brace) 

708 

709 detail = kwargs.pop("detail", False) 

710 withdirs = kwargs.pop("withdirs", True) 

711 

712 if not has_magic(path): 

713 if self.exists(path, **kwargs): 

714 if not detail: 

715 return [path] 

716 else: 

717 return {path: self.info(path, **kwargs)} 

718 else: 

719 if not detail: 

720 return [] # glob of non-existent returns empty 

721 else: 

722 return {} 

723 elif "/" in path[:min_idx]: 

724 min_idx = path[:min_idx].rindex("/") 

725 root = path[: min_idx + 1] 

726 depth = path[min_idx + 1 :].count("/") + 1 

727 else: 

728 root = "" 

729 depth = path[min_idx + 1 :].count("/") + 1 

730 

731 if "**" in path: 

732 if maxdepth is not None: 

733 idx_double_stars = path.find("**") 

734 depth_double_stars = path[idx_double_stars:].count("/") + 1 

735 depth = depth - depth_double_stars + maxdepth 

736 else: 

737 depth = None 

738 

739 allpaths = self.find( 

740 root, maxdepth=depth, withdirs=withdirs, detail=True, **kwargs 

741 ) 

742 

743 pattern = glob_translate(path + ("/" if ends_with_sep else "")) 

744 pattern = re.compile(pattern) 

745 

746 out = { 

747 p: info 

748 for p, info in sorted(allpaths.items()) 

749 if pattern.match( 

750 p + "/" 

751 if append_slash_to_dirname and info["type"] == "directory" 

752 else p 

753 ) 

754 } 

755 

756 if detail: 

757 return out 

758 else: 

759 return list(out) 

760 

761 def exists(self, path, **kwargs): 

762 """Is there a file at the given path""" 

763 try: 

764 self.info(path, **kwargs) 

765 return True 

766 except: # noqa: E722 

767 # any exception allowed bar FileNotFoundError? 

768 return False 

769 

770 def lexists(self, path, **kwargs): 

771 """If there is a file at the given path (including 

772 broken links)""" 

773 return self.exists(path) 

774 

775 def info(self, path, **kwargs): 

776 """Give details of entry at path 

777 

778 Returns a single dictionary, with exactly the same information as ``ls`` 

779 would with ``detail=True``. 

780 

781 The default implementation calls ls and could be overridden by a 

782 shortcut. kwargs are passed on to ```ls()``. 

783 

784 Some file systems might not be able to measure the file's size, in 

785 which case, the returned dict will include ``'size': None``. 

786 

787 Returns 

788 ------- 

789 dict with keys: name (full path in the FS), size (in bytes), type (file, 

790 directory, or something else) and other FS-specific keys. 

791 """ 

792 path = self._strip_protocol(path) 

793 out = self.ls(self._parent(path), detail=True, **kwargs) 

794 out = [o for o in out if o["name"].rstrip("/") == path] 

795 if out: 

796 return out[0] 

797 out = self.ls(path, detail=True, **kwargs) 

798 path = path.rstrip("/") 

799 out1 = [o for o in out if o["name"].rstrip("/") == path] 

800 if len(out1) == 1: 

801 if "size" not in out1[0]: 

802 out1[0]["size"] = None 

803 return out1[0] 

804 elif len(out1) > 1 or out: 

805 return {"name": path, "size": 0, "type": "directory"} 

806 else: 

807 raise FileNotFoundError(path) 

808 

809 def checksum(self, path): 

810 """Unique value for current version of file 

811 

812 If the checksum is the same from one moment to another, the contents 

813 are guaranteed to be the same. If the checksum changes, the contents 

814 *might* have changed. 

815 

816 This should normally be overridden; default will probably capture 

817 creation/modification timestamp (which would be good) or maybe 

818 access timestamp (which would be bad) 

819 """ 

820 return int(tokenize(self.info(path)), 16) 

821 

822 def size(self, path): 

823 """Size in bytes of file""" 

824 return self.info(path).get("size", None) 

825 

826 def sizes(self, paths): 

827 """Size in bytes of each file in a list of paths""" 

828 return [self.size(p) for p in paths] 

829 

830 def isdir(self, path): 

831 """Is this entry directory-like?""" 

832 try: 

833 return self.info(path)["type"] == "directory" 

834 except OSError: 

835 return False 

836 

837 def isfile(self, path): 

838 """Is this entry file-like?""" 

839 try: 

840 return self.info(path)["type"] == "file" 

841 except Exception: 

842 return False 

843 

844 def read_text(self, path, encoding=None, errors=None, newline=None, **kwargs): 

845 """Get the contents of the file as a string. 

846 

847 Parameters 

848 ---------- 

849 path: str 

850 URL of file on this filesystems 

851 encoding, errors, newline: same as `open`. 

852 """ 

853 with self.open( 

854 path, 

855 mode="r", 

856 encoding=encoding, 

857 errors=errors, 

858 newline=newline, 

859 **kwargs, 

860 ) as f: 

861 return f.read() 

862 

863 def write_text( 

864 self, path, value, encoding=None, errors=None, newline=None, **kwargs 

865 ): 

866 """Write the text to the given file. 

867 

868 An existing file will be overwritten. 

869 

870 Parameters 

871 ---------- 

872 path: str 

873 URL of file on this filesystems 

874 value: str 

875 Text to write. 

876 encoding, errors, newline: same as `open`. 

877 """ 

878 with self.open( 

879 path, 

880 mode="w", 

881 encoding=encoding, 

882 errors=errors, 

883 newline=newline, 

884 **kwargs, 

885 ) as f: 

886 return f.write(value) 

887 

888 def cat_file(self, path, start=None, end=None, **kwargs): 

889 """Get the content of a file 

890 

891 Parameters 

892 ---------- 

893 path: URL of file on this filesystems 

894 start, end: int 

895 Bytes limits of the read. If negative, backwards from end, 

896 like usual python slices. Either can be None for start or 

897 end of file, respectively 

898 kwargs: passed to ``open()``. 

899 """ 

900 # explicitly set buffering off? 

901 with self.open(path, "rb", **kwargs) as f: 

902 # A negative start or end counts back from the file length. Only 

903 # fsspec's own file classes expose `.size`; a plain file (e.g. from 

904 # the caching filesystems) does not, so measure with a seek instead. 

905 needs_size = (start is not None and start < 0) or ( 

906 end is not None and end < 0 

907 ) 

908 size = None 

909 seeked_for_size = False 

910 if needs_size: 

911 size = getattr(f, "size", None) 

912 if size is None: 

913 size = f.seek(0, 2) 

914 seeked_for_size = True 

915 if start is not None: 

916 if start >= 0: 

917 f.seek(start) 

918 else: 

919 f.seek(max(0, size + start)) 

920 elif seeked_for_size: 

921 # measuring the size above moved the cursor to the end; with no 

922 # start given, put it back so the read begins at the start. When 

923 # .size was used instead, or nothing was measured, the cursor 

924 # is untouched, as before, so a plain full read still works 

925 # on non-seekable files. 

926 f.seek(0) 

927 if end is not None: 

928 if end < 0: 

929 # a negative end counts back from the end, as the start 

930 # does, and clamps at the start of the file 

931 end = max(0, size + end) 

932 # a range that ends at or before where it starts is empty; a 

933 # negative length would instead mean "read to the end" 

934 return f.read(max(0, end - f.tell())) 

935 return f.read() 

936 

937 def pipe_file(self, path, value, mode="overwrite", **kwargs): 

938 """Set the bytes of given file""" 

939 if mode == "create" and self.exists(path): 

940 # non-atomic but simple way; or could use "xb" in open(), which is likely 

941 # not as well supported 

942 raise FileExistsError 

943 with self.open(path, "wb", **kwargs) as f: 

944 f.write(value) 

945 

946 def pipe(self, path, value=None, **kwargs): 

947 """Put value into path 

948 

949 (counterpart to ``cat``) 

950 

951 Parameters 

952 ---------- 

953 path: string or dict(str, bytes) 

954 If a string, a single remote location to put ``value`` bytes; if a dict, 

955 a mapping of {path: bytesvalue}. 

956 value: bytes, optional 

957 If using a single path, these are the bytes to put there. Ignored if 

958 ``path`` is a dict 

959 """ 

960 if isinstance(path, str): 

961 self.pipe_file(self._strip_protocol(path), value, **kwargs) 

962 elif isinstance(path, dict): 

963 for k, v in path.items(): 

964 self.pipe_file(self._strip_protocol(k), v, **kwargs) 

965 else: 

966 raise ValueError("path must be str or dict") 

967 

968 def cat_ranges( 

969 self, paths, starts, ends, max_gap=None, on_error="return", **kwargs 

970 ): 

971 """Get the contents of byte ranges from one or more files 

972 

973 Parameters 

974 ---------- 

975 paths: list 

976 A list of of filepaths on this filesystems 

977 starts, ends: int or list 

978 Bytes limits of the read. If using a single int, the same value will be 

979 used to read all the specified files. 

980 """ 

981 if max_gap is not None: 

982 raise NotImplementedError 

983 if not isinstance(paths, list): 

984 raise TypeError 

985 if not isinstance(starts, list): 

986 starts = [starts] * len(paths) 

987 if not isinstance(ends, list): 

988 ends = [ends] * len(paths) 

989 if len(starts) != len(paths) or len(ends) != len(paths): 

990 raise ValueError 

991 out = [] 

992 for p, s, e in zip(paths, starts, ends): 

993 try: 

994 out.append(self.cat_file(p, s, e, **kwargs)) 

995 except Exception as e: 

996 if on_error == "return": 

997 out.append(e) 

998 else: 

999 raise 

1000 return out 

1001 

1002 def cat(self, path, recursive=False, on_error="raise", **kwargs): 

1003 """Fetch (potentially multiple) paths' contents 

1004 

1005 Parameters 

1006 ---------- 

1007 recursive: bool 

1008 If True, assume the path(s) are directories, and get all the 

1009 contained files 

1010 on_error : "raise", "omit", "return" 

1011 If raise, an underlying exception will be raised (converted to KeyError 

1012 if the type is in self.missing_exceptions); if omit, keys with exception 

1013 will simply not be included in the output; if "return", all keys are 

1014 included in the output, but the value will be bytes or an exception 

1015 instance. 

1016 kwargs: passed to cat_file 

1017 

1018 Returns 

1019 ------- 

1020 dict of {path: contents} if there are multiple paths 

1021 or the path has been otherwise expanded 

1022 """ 

1023 paths = self.expand_path(path, recursive=recursive, **kwargs) 

1024 if ( 

1025 len(paths) > 1 

1026 or isinstance(path, list) 

1027 or paths[0] != self._strip_protocol(path) 

1028 ): 

1029 out = {} 

1030 for path in paths: 

1031 try: 

1032 out[path] = self.cat_file(path, **kwargs) 

1033 except Exception as e: 

1034 if on_error == "raise": 

1035 raise 

1036 if on_error == "return": 

1037 out[path] = e 

1038 return out 

1039 else: 

1040 return self.cat_file(paths[0], **kwargs) 

1041 

1042 def get_file( 

1043 self, rpath, lpath=None, callback=DEFAULT_CALLBACK, outfile=None, **kwargs 

1044 ): 

1045 """Copy single remote file to local""" 

1046 from .implementations.local import LocalFileSystem 

1047 

1048 if outfile is None and isfilelike(lpath): 

1049 outfile = lpath 

1050 elif outfile is None and self.isdir(rpath): 

1051 os.makedirs(lpath, exist_ok=True) 

1052 return None 

1053 

1054 if outfile is None: 

1055 fs = LocalFileSystem(auto_mkdir=True) 

1056 fs.makedirs(fs._parent(lpath), exist_ok=True) 

1057 

1058 with self.open(rpath, "rb", **kwargs) as f1: 

1059 close_outfile = outfile is None 

1060 if close_outfile: 

1061 outfile = open(lpath, "wb") 

1062 

1063 try: 

1064 callback.set_size(getattr(f1, "size", None)) 

1065 data = True 

1066 while data: 

1067 data = f1.read(self.blocksize) 

1068 segment_len = outfile.write(data) 

1069 if segment_len is None: 

1070 segment_len = len(data) 

1071 callback.relative_update(segment_len) 

1072 finally: 

1073 if close_outfile: 

1074 outfile.close() 

1075 

1076 def get( 

1077 self, 

1078 rpath, 

1079 lpath, 

1080 recursive=False, 

1081 callback=DEFAULT_CALLBACK, 

1082 maxdepth=None, 

1083 **kwargs, 

1084 ): 

1085 """Copy file(s) to local. 

1086 

1087 Copies a specific file or tree of files (if recursive=True). If lpath 

1088 ends with a "/", it will be assumed to be a directory, and target files 

1089 will go within. Can submit a list of paths, which may be glob-patterns 

1090 and will be expanded. 

1091 

1092 Calls get_file for each source. 

1093 """ 

1094 if isinstance(lpath, list) and isinstance(rpath, list): 

1095 # No need to expand paths when both source and destination 

1096 # are provided as lists 

1097 rpaths = rpath 

1098 lpaths = lpath 

1099 else: 

1100 from .implementations.local import ( 

1101 LocalFileSystem, 

1102 make_path_posix, 

1103 trailing_sep, 

1104 ) 

1105 

1106 rpath = stringify_path(rpath) 

1107 source_is_str = isinstance(rpath, str) 

1108 rpaths = self.expand_path( 

1109 rpath, recursive=recursive, maxdepth=maxdepth, **kwargs 

1110 ) 

1111 if source_is_str and (not recursive or maxdepth is not None): 

1112 # Non-recursive glob does not copy directories 

1113 rpaths = [p for p in rpaths if not (trailing_sep(p) or self.isdir(p))] 

1114 if not rpaths: 

1115 return 

1116 

1117 if isinstance(lpath, (str, os.PathLike)): 

1118 lpath = make_path_posix(lpath) 

1119 

1120 source_is_file = len(rpaths) == 1 

1121 dest_is_dir = isinstance(lpath, str) and ( 

1122 trailing_sep(lpath) or LocalFileSystem().isdir(lpath) 

1123 ) 

1124 

1125 exists = source_is_str and ( 

1126 (has_magic(rpath) and source_is_file) 

1127 or (not has_magic(rpath) and dest_is_dir and not trailing_sep(rpath)) 

1128 ) 

1129 lpaths = other_paths( 

1130 rpaths, 

1131 lpath, 

1132 exists=exists, 

1133 flatten=not source_is_str, 

1134 ) 

1135 if isinstance(lpath, str): 

1136 # The names came from the source listing; ".." in one of them 

1137 # would otherwise place the copy above the destination. When 

1138 # lpath is a list the caller named every destination itself. 

1139 check_contained(lpath, lpaths) 

1140 

1141 callback.set_size(len(lpaths)) 

1142 for lpath, rpath in callback.wrap(zip(lpaths, rpaths)): 

1143 with callback.branched(rpath, lpath) as child: 

1144 self.get_file(rpath, lpath, callback=child, **kwargs) 

1145 

1146 def put_file( 

1147 self, lpath, rpath, callback=DEFAULT_CALLBACK, mode="overwrite", **kwargs 

1148 ): 

1149 """Copy single file to remote""" 

1150 if mode == "create" and self.exists(rpath): 

1151 raise FileExistsError 

1152 if os.path.isdir(lpath): 

1153 self.makedirs(rpath, exist_ok=True) 

1154 return None 

1155 

1156 with open(lpath, "rb") as f1: 

1157 size = f1.seek(0, 2) 

1158 callback.set_size(size) 

1159 f1.seek(0) 

1160 

1161 self.mkdirs(self._parent(os.fspath(rpath)), exist_ok=True) 

1162 with self.open(rpath, "wb", **kwargs) as f2: 

1163 while f1.tell() < size: 

1164 data = f1.read(self.blocksize) 

1165 segment_len = f2.write(data) 

1166 if segment_len is None: 

1167 segment_len = len(data) 

1168 callback.relative_update(segment_len) 

1169 

1170 def put( 

1171 self, 

1172 lpath, 

1173 rpath, 

1174 recursive=False, 

1175 callback=DEFAULT_CALLBACK, 

1176 maxdepth=None, 

1177 **kwargs, 

1178 ): 

1179 """Copy file(s) from local. 

1180 

1181 Copies a specific file or tree of files (if recursive=True). If rpath 

1182 ends with a "/", it will be assumed to be a directory, and target files 

1183 will go within. 

1184 

1185 Calls put_file for each source. 

1186 """ 

1187 if isinstance(lpath, list) and isinstance(rpath, list): 

1188 # No need to expand paths when both source and destination 

1189 # are provided as lists 

1190 rpaths = rpath 

1191 lpaths = lpath 

1192 else: 

1193 from .implementations.local import ( 

1194 LocalFileSystem, 

1195 make_path_posix, 

1196 trailing_sep, 

1197 ) 

1198 

1199 lpath = stringify_path(lpath) 

1200 source_is_str = isinstance(lpath, str) 

1201 if source_is_str: 

1202 lpath = make_path_posix(lpath) 

1203 fs = LocalFileSystem() 

1204 lpaths = fs.expand_path( 

1205 lpath, recursive=recursive, maxdepth=maxdepth, **kwargs 

1206 ) 

1207 if source_is_str and (not recursive or maxdepth is not None): 

1208 # Non-recursive glob does not copy directories 

1209 lpaths = [p for p in lpaths if not (trailing_sep(p) or fs.isdir(p))] 

1210 if not lpaths: 

1211 return 

1212 

1213 source_is_file = len(lpaths) == 1 

1214 dest_is_dir = isinstance(rpath, str) and ( 

1215 trailing_sep(rpath) or self.isdir(rpath) 

1216 ) 

1217 

1218 rpath = ( 

1219 self._strip_protocol(rpath) 

1220 if isinstance(rpath, str) 

1221 else [self._strip_protocol(p) for p in rpath] 

1222 ) 

1223 exists = source_is_str and ( 

1224 (has_magic(lpath) and source_is_file) 

1225 or (not has_magic(lpath) and dest_is_dir and not trailing_sep(lpath)) 

1226 ) 

1227 rpaths = other_paths( 

1228 lpaths, 

1229 rpath, 

1230 exists=exists, 

1231 flatten=not source_is_str, 

1232 ) 

1233 

1234 callback.set_size(len(rpaths)) 

1235 for lpath, rpath in callback.wrap(zip(lpaths, rpaths)): 

1236 with callback.branched(lpath, rpath) as child: 

1237 self.put_file(lpath, rpath, callback=child, **kwargs) 

1238 

1239 def head(self, path, size=1024): 

1240 """Get the first ``size`` bytes from file""" 

1241 with self.open(path, "rb") as f: 

1242 return f.read(size) 

1243 

1244 def tail(self, path, size=1024): 

1245 """Get the last ``size`` bytes from file""" 

1246 with self.open(path, "rb") as f: 

1247 f.seek(0, 2) 

1248 f.seek(max(f.tell() - size, 0)) 

1249 return f.read() 

1250 

1251 def cp_file(self, path1, path2, **kwargs): 

1252 raise NotImplementedError 

1253 

1254 def copy( 

1255 self, path1, path2, recursive=False, maxdepth=None, on_error=None, **kwargs 

1256 ): 

1257 """Copy within two locations in the filesystem 

1258 

1259 on_error : "raise", "ignore" 

1260 If raise, any not-found exceptions will be raised; if ignore any 

1261 not-found exceptions will cause the path to be skipped; defaults to 

1262 raise unless recursive is true, where the default is ignore 

1263 """ 

1264 if on_error is None and recursive: 

1265 on_error = "ignore" 

1266 elif on_error is None: 

1267 on_error = "raise" 

1268 

1269 if isinstance(path1, list) and isinstance(path2, list): 

1270 # No need to expand paths when both source and destination 

1271 # are provided as lists 

1272 paths1 = path1 

1273 paths2 = path2 

1274 else: 

1275 from .implementations.local import trailing_sep 

1276 

1277 path1 = stringify_path(path1) 

1278 source_is_str = isinstance(path1, str) 

1279 paths1 = self.expand_path( 

1280 path1, recursive=recursive, maxdepth=maxdepth, **kwargs 

1281 ) 

1282 if source_is_str and (not recursive or maxdepth is not None): 

1283 # Non-recursive glob does not copy directories 

1284 paths1 = [p for p in paths1 if not (trailing_sep(p) or self.isdir(p))] 

1285 if not paths1: 

1286 return 

1287 

1288 source_is_file = len(paths1) == 1 

1289 dest_is_dir = isinstance(path2, str) and ( 

1290 trailing_sep(path2) or self.isdir(path2) 

1291 ) 

1292 

1293 exists = source_is_str and ( 

1294 (has_magic(path1) and source_is_file) 

1295 or (not has_magic(path1) and dest_is_dir and not trailing_sep(path1)) 

1296 ) 

1297 paths2 = other_paths( 

1298 paths1, 

1299 path2, 

1300 exists=exists, 

1301 flatten=not source_is_str, 

1302 ) 

1303 

1304 for p1, p2 in zip(paths1, paths2): 

1305 try: 

1306 self.cp_file(p1, p2, **kwargs) 

1307 except FileNotFoundError: 

1308 if on_error == "raise": 

1309 raise 

1310 

1311 def expand_path( 

1312 self, path, recursive=False, maxdepth=None, assume_literal=False, **kwargs 

1313 ): 

1314 """Turn one or more globs or directories into a list of all matching paths 

1315 to files or directories. 

1316 

1317 kwargs are passed to ``glob`` or ``find``, which may in turn call ``ls`` 

1318 """ 

1319 

1320 if maxdepth is not None and maxdepth < 1: 

1321 raise ValueError("maxdepth must be at least 1") 

1322 

1323 if isinstance(path, (str, os.PathLike)): 

1324 out = self.expand_path([path], recursive, maxdepth, **kwargs) 

1325 else: 

1326 out = set() 

1327 path = [self._strip_protocol(p) for p in path] 

1328 for p in path: 

1329 if not assume_literal and has_magic(p): 

1330 bit = set(self.glob(p, maxdepth=maxdepth, **kwargs)) 

1331 out |= bit 

1332 if recursive: 

1333 # glob call above expanded one depth so if maxdepth is defined 

1334 # then decrement it in expand_path call below. If it is zero 

1335 # after decrementing then avoid expand_path call. 

1336 if maxdepth is not None and maxdepth <= 1: 

1337 continue 

1338 out |= set( 

1339 self.expand_path( 

1340 list(bit), 

1341 recursive=recursive, 

1342 maxdepth=maxdepth - 1 if maxdepth is not None else None, 

1343 assume_literal=True, 

1344 **kwargs, 

1345 ) 

1346 ) 

1347 continue 

1348 elif recursive: 

1349 rec = set( 

1350 self.find( 

1351 p, maxdepth=maxdepth, withdirs=True, detail=False, **kwargs 

1352 ) 

1353 ) 

1354 out |= rec 

1355 if p not in out and (recursive is False or self.exists(p)): 

1356 # should only check once, for the root 

1357 out.add(p) 

1358 if not out: 

1359 raise FileNotFoundError(path) 

1360 return sorted(out) 

1361 

1362 def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs): 

1363 """Move file(s) from one location to another""" 

1364 if path1 == path2: 

1365 logger.debug("%s mv: The paths are the same, so no files were moved.", self) 

1366 else: 

1367 if recursive and maxdepth is not None: 

1368 # copy only reaches maxdepth, so only remove what it copied 

1369 paths = self.expand_path(path1, recursive=True, maxdepth=maxdepth) 

1370 dirs = [p for p in paths if self.isdir(p)] 

1371 files = [p for p in paths if p not in dirs] 

1372 # explicitly raise exception to prevent data corruption 

1373 self.copy( 

1374 path1, path2, recursive=recursive, maxdepth=maxdepth, on_error="raise" 

1375 ) 

1376 if recursive and maxdepth is not None: 

1377 if files: 

1378 self.rm(files) 

1379 # a directory still holding files deeper than maxdepth is kept 

1380 for p in reversed(dirs): 

1381 if self.exists(p) and not self.ls(p, detail=False): 

1382 self.rmdir(p) 

1383 else: 

1384 self.rm(path1, recursive=recursive) 

1385 

1386 def rm_file(self, path): 

1387 """Delete a file""" 

1388 self._rm(path) 

1389 

1390 def _rm(self, path): 

1391 """Delete one file""" 

1392 # this is the old name for the method, prefer rm_file 

1393 raise NotImplementedError 

1394 

1395 def rm(self, path, recursive=False, maxdepth=None): 

1396 """Delete files or directories. 

1397 

1398 Parameters 

1399 ---------- 

1400 path: str or list of str 

1401 Files or directories to delete. 

1402 recursive: bool 

1403 If True, recursively delete directories and their contents. 

1404 maxdepth: int or None 

1405 Depth to pass to walk for finding files to delete, if recursive. 

1406 If None, there will be no limit and infinite recursion may be 

1407 possible. 

1408 """ 

1409 path = self.expand_path(path, recursive=recursive, maxdepth=maxdepth) 

1410 for p in reversed(path): 

1411 self.rm_file(p) 

1412 

1413 @classmethod 

1414 def _parent(cls, path): 

1415 path = cls._strip_protocol(path) 

1416 if "/" in path: 

1417 parent = path.rsplit("/", 1)[0].lstrip(cls.root_marker) 

1418 return cls.root_marker + parent 

1419 else: 

1420 return cls.root_marker 

1421 

1422 def _open( 

1423 self, 

1424 path, 

1425 mode="rb", 

1426 block_size=None, 

1427 autocommit=True, 

1428 cache_options=None, 

1429 **kwargs, 

1430 ): 

1431 """Return raw bytes-mode file-like from the file-system""" 

1432 return AbstractBufferedFile( 

1433 self, 

1434 path, 

1435 mode, 

1436 block_size, 

1437 autocommit, 

1438 cache_options=cache_options, 

1439 **kwargs, 

1440 ) 

1441 

1442 def open( 

1443 self, 

1444 path, 

1445 mode="rb", 

1446 block_size=None, 

1447 cache_options=None, 

1448 compression=None, 

1449 **kwargs, 

1450 ): 

1451 """ 

1452 Return a file-like object from the filesystem 

1453 

1454 The resultant instance must function correctly in a context ``with`` 

1455 block. 

1456 

1457 Parameters 

1458 ---------- 

1459 path: str 

1460 Target file 

1461 mode: str like 'rb', 'w' 

1462 See builtin ``open()`` 

1463 Mode "x" (exclusive write) may be implemented by the backend. Even if 

1464 it is, whether it is checked up front or on commit, and whether it is 

1465 atomic is implementation-dependent. 

1466 block_size: int 

1467 Some indication of buffering - this is a value in bytes 

1468 cache_options : dict, optional 

1469 Extra arguments to pass through to the cache. 

1470 compression: string or None 

1471 If given, open file using compression codec. Can either be a compression 

1472 name (a key in ``fsspec.compression.compr``) or "infer" to guess the 

1473 compression from the filename suffix. 

1474 encoding, errors, newline: passed on to TextIOWrapper for text mode 

1475 """ 

1476 import io 

1477 

1478 path = self._strip_protocol(path) 

1479 if "b" not in mode: 

1480 mode = mode.replace("t", "") + "b" 

1481 

1482 text_kwargs = { 

1483 k: kwargs.pop(k) 

1484 for k in ["encoding", "errors", "newline"] 

1485 if k in kwargs 

1486 } 

1487 return io.TextIOWrapper( 

1488 self.open( 

1489 path, 

1490 mode, 

1491 block_size=block_size, 

1492 cache_options=cache_options, 

1493 compression=compression, 

1494 **kwargs, 

1495 ), 

1496 **text_kwargs, 

1497 ) 

1498 else: 

1499 ac = kwargs.pop("autocommit", not self._intrans) 

1500 f = self._open( 

1501 path, 

1502 mode=mode, 

1503 block_size=block_size, 

1504 autocommit=ac, 

1505 cache_options=cache_options, 

1506 **kwargs, 

1507 ) 

1508 if not ac and "r" not in mode: 

1509 self.transaction.files.append(f) 

1510 if compression is not None: 

1511 from fsspec.compression import compr 

1512 from fsspec.core import get_compression 

1513 

1514 compression = get_compression(path, compression) 

1515 compress = compr[compression] 

1516 f = compress(f, mode=mode[0]) 

1517 return f 

1518 

1519 def touch(self, path, truncate=True, **kwargs): 

1520 """Create empty file, or update timestamp 

1521 

1522 Parameters 

1523 ---------- 

1524 path: str 

1525 file location 

1526 truncate: bool 

1527 If True, always set file size to 0; if False, update timestamp and 

1528 leave file unchanged, if backend allows this 

1529 """ 

1530 if truncate or not self.exists(path): 

1531 with self.open(path, "wb", **kwargs): 

1532 pass 

1533 else: 

1534 raise NotImplementedError # update timestamp, if possible 

1535 

1536 def ukey(self, path): 

1537 """Hash of file properties, to tell if it has changed""" 

1538 return sha256(str(self.info(path)).encode()).hexdigest() 

1539 

1540 def read_block(self, fn, offset, length, delimiter=None): 

1541 """Read a block of bytes from 

1542 

1543 Starting at ``offset`` of the file, read ``length`` bytes. If 

1544 ``delimiter`` is set then we ensure that the read starts and stops at 

1545 delimiter boundaries that follow the locations ``offset`` and ``offset 

1546 + length``. If ``offset`` is zero then we start at zero. The 

1547 bytestring returned WILL include the end delimiter string. 

1548 

1549 If offset+length is beyond the eof, reads to eof. 

1550 

1551 Parameters 

1552 ---------- 

1553 fn: string 

1554 Path to filename 

1555 offset: int 

1556 Byte offset to start read 

1557 length: int 

1558 Number of bytes to read. If None, read to end. 

1559 delimiter: bytes (optional) 

1560 Ensure reading starts and stops at delimiter bytestring 

1561 

1562 Examples 

1563 -------- 

1564 >>> fs.read_block('data/file.csv', 0, 13) # doctest: +SKIP 

1565 b'Alice, 100\\nBo' 

1566 >>> fs.read_block('data/file.csv', 0, 13, delimiter=b'\\n') # doctest: +SKIP 

1567 b'Alice, 100\\nBob, 200\\n' 

1568 

1569 Use ``length=None`` to read to the end of the file. 

1570 >>> fs.read_block('data/file.csv', 0, None, delimiter=b'\\n') # doctest: +SKIP 

1571 b'Alice, 100\\nBob, 200\\nCharlie, 300' 

1572 

1573 See Also 

1574 -------- 

1575 :func:`fsspec.utils.read_block` 

1576 """ 

1577 with self.open(fn, "rb") as f: 

1578 # Only fsspec's own file classes expose `.size`; a plain file (e.g. 

1579 # from the caching filesystems) does not, so fall back to a seek. 

1580 size = getattr(f, "size", None) 

1581 if size is None: 

1582 size = f.seek(0, 2) 

1583 f.seek(0) 

1584 if length is None: 

1585 length = size 

1586 if size is not None and offset + length > size: 

1587 length = max(0, size - offset) 

1588 return read_block(f, offset, length, delimiter) 

1589 

1590 def to_json(self, *, include_password: bool = True) -> str: 

1591 """ 

1592 JSON representation of this filesystem instance. 

1593 

1594 Parameters 

1595 ---------- 

1596 include_password: bool, default True 

1597 Whether to include the password (if any) in the output. 

1598 

1599 Returns 

1600 ------- 

1601 JSON string with keys ``cls`` (the python location of this class), 

1602 protocol (text name of this class's protocol, first one in case of 

1603 multiple), ``args`` (positional args, usually empty), and all other 

1604 keyword arguments as their own keys. 

1605 

1606 Warnings 

1607 -------- 

1608 Serialized filesystems may contain sensitive information which have been 

1609 passed to the constructor, such as passwords and tokens. Make sure you 

1610 store and send them in a secure environment! 

1611 """ 

1612 from .json import FilesystemJSONEncoder 

1613 

1614 return json.dumps( 

1615 self, 

1616 cls=type( 

1617 "_FilesystemJSONEncoder", 

1618 (FilesystemJSONEncoder,), 

1619 {"include_password": include_password}, 

1620 ), 

1621 ) 

1622 

1623 @staticmethod 

1624 def from_json(blob: str) -> AbstractFileSystem: 

1625 """ 

1626 Recreate a filesystem instance from JSON representation. 

1627 

1628 See ``.to_json()`` for the expected structure of the input. 

1629 

1630 Parameters 

1631 ---------- 

1632 blob: str 

1633 

1634 Returns 

1635 ------- 

1636 file system instance, not necessarily of this particular class. 

1637 

1638 Warnings 

1639 -------- 

1640 This can import arbitrary modules (as determined by the ``cls`` key). 

1641 Make sure you haven't installed any modules that may execute malicious code 

1642 at import time. 

1643 """ 

1644 from .json import FilesystemJSONDecoder 

1645 

1646 return json.loads(blob, cls=FilesystemJSONDecoder) 

1647 

1648 def to_dict(self, *, include_password: bool = True) -> dict[str, Any]: 

1649 """ 

1650 JSON-serializable dictionary representation of this filesystem instance. 

1651 

1652 Parameters 

1653 ---------- 

1654 include_password: bool, default True 

1655 Whether to include the password (if any) in the output. 

1656 

1657 Returns 

1658 ------- 

1659 Dictionary with keys ``cls`` (the python location of this class), 

1660 protocol (text name of this class's protocol, first one in case of 

1661 multiple), ``args`` (positional args, usually empty), and all other 

1662 keyword arguments as their own keys. 

1663 

1664 Warnings 

1665 -------- 

1666 Serialized filesystems may contain sensitive information which have been 

1667 passed to the constructor, such as passwords and tokens. Make sure you 

1668 store and send them in a secure environment! 

1669 """ 

1670 from .json import FilesystemJSONEncoder 

1671 

1672 json_encoder = type( 

1673 "_FilesystemJSONEncoder", 

1674 (FilesystemJSONEncoder,), 

1675 {"include_password": include_password}, 

1676 )() 

1677 

1678 cls = type(self) 

1679 proto = self.protocol 

1680 

1681 storage_options = dict(self.storage_options) 

1682 if not include_password: 

1683 storage_options.pop("password", None) 

1684 

1685 return dict( 

1686 cls=f"{cls.__module__}:{cls.__name__}", 

1687 protocol=proto[0] if isinstance(proto, (tuple, list)) else proto, 

1688 args=json_encoder.make_serializable(self.storage_args), 

1689 **json_encoder.make_serializable(storage_options), 

1690 ) 

1691 

1692 @staticmethod 

1693 def from_dict(dct: dict[str, Any]) -> AbstractFileSystem: 

1694 """ 

1695 Recreate a filesystem instance from dictionary representation. 

1696 

1697 See ``.to_dict()`` for the expected structure of the input. 

1698 

1699 Parameters 

1700 ---------- 

1701 dct: Dict[str, Any] 

1702 

1703 Returns 

1704 ------- 

1705 file system instance, not necessarily of this particular class. 

1706 

1707 Warnings 

1708 -------- 

1709 This can import arbitrary modules (as determined by the ``cls`` key). 

1710 Make sure you haven't installed any modules that may execute malicious code 

1711 at import time. 

1712 """ 

1713 from .json import FilesystemJSONDecoder 

1714 

1715 json_decoder = FilesystemJSONDecoder() 

1716 

1717 dct = dict(dct) # Defensive copy 

1718 

1719 cls = FilesystemJSONDecoder.try_resolve_fs_cls(dct) 

1720 if cls is None: 

1721 raise ValueError("Not a serialized AbstractFileSystem") 

1722 

1723 dct.pop("cls", None) 

1724 dct.pop("protocol", None) 

1725 

1726 return cls( 

1727 *json_decoder.unmake_serializable(dct.pop("args", ())), 

1728 **json_decoder.unmake_serializable(dct), 

1729 ) 

1730 

1731 def _get_pyarrow_filesystem(self): 

1732 """ 

1733 Make a version of the FS instance which will be acceptable to pyarrow 

1734 """ 

1735 # all instances already also derive from pyarrow 

1736 return self 

1737 

1738 def get_mapper(self, root="", check=False, create=False, missing_exceptions=None): 

1739 """Create key/value store based on this file-system 

1740 

1741 Makes a MutableMapping interface to the FS at the given root path. 

1742 See ``fsspec.mapping.FSMap`` for further details. 

1743 """ 

1744 from .mapping import FSMap 

1745 

1746 return FSMap( 

1747 root, 

1748 self, 

1749 check=check, 

1750 create=create, 

1751 missing_exceptions=missing_exceptions, 

1752 ) 

1753 

1754 @classmethod 

1755 def clear_instance_cache(cls): 

1756 """ 

1757 Clear the cache of filesystem instances. 

1758 

1759 Notes 

1760 ----- 

1761 Unless overridden by setting the ``cachable`` class attribute to False, 

1762 the filesystem class stores a reference to newly created instances. This 

1763 prevents Python's normal rules around garbage collection from working, 

1764 since the instances refcount will not drop to zero until 

1765 ``clear_instance_cache`` is called. 

1766 """ 

1767 cls._cache.clear() 

1768 

1769 def created(self, path): 

1770 """Return the created timestamp of a file as a datetime.datetime""" 

1771 raise NotImplementedError 

1772 

1773 def modified(self, path): 

1774 """Return the modified timestamp of a file as a datetime.datetime""" 

1775 raise NotImplementedError 

1776 

1777 def tree( 

1778 self, 

1779 path: str = "/", 

1780 recursion_limit: int = 2, 

1781 max_display: int = 25, 

1782 display_size: bool = False, 

1783 prefix: str = "", 

1784 is_last: bool = True, 

1785 first: bool = True, 

1786 indent_size: int = 4, 

1787 ) -> str: 

1788 """ 

1789 Return a tree-like structure of the filesystem starting from the given path as a string. 

1790 

1791 Parameters 

1792 ---------- 

1793 path: Root path to start traversal from 

1794 recursion_limit: Maximum depth of directory traversal 

1795 max_display: Maximum number of items to display per directory 

1796 display_size: Whether to display file sizes 

1797 prefix: Current line prefix for visual tree structure 

1798 is_last: Whether current item is last in its level 

1799 first: Whether this is the first call (displays root path) 

1800 indent_size: Number of spaces by indent 

1801 

1802 Returns 

1803 ------- 

1804 str: A string representing the tree structure. 

1805 

1806 Example 

1807 ------- 

1808 >>> from fsspec import filesystem 

1809 

1810 >>> fs = filesystem('ftp', host='test.rebex.net', user='demo', password='password') 

1811 >>> tree = fs.tree(display_size=True, recursion_limit=3, indent_size=8, max_display=10) 

1812 >>> print(tree) 

1813 """ 

1814 

1815 def format_bytes(n: int) -> str: 

1816 """Format bytes as text.""" 

1817 for prefix, k in ( 

1818 ("P", 2**50), 

1819 ("T", 2**40), 

1820 ("G", 2**30), 

1821 ("M", 2**20), 

1822 ("k", 2**10), 

1823 ): 

1824 if n >= 0.9 * k: 

1825 return f"{n / k:.2f} {prefix}b" 

1826 return f"{n}B" 

1827 

1828 result = [] 

1829 

1830 if first: 

1831 result.append(path) 

1832 

1833 if recursion_limit: 

1834 indent = " " * indent_size 

1835 contents = self.ls(path, detail=True) 

1836 contents.sort( 

1837 key=lambda x: (x.get("type") != "directory", x.get("name", "")) 

1838 ) 

1839 

1840 if max_display is not None and len(contents) > max_display: 

1841 displayed_contents = contents[:max_display] 

1842 remaining_count = len(contents) - max_display 

1843 else: 

1844 displayed_contents = contents 

1845 remaining_count = 0 

1846 

1847 for i, item in enumerate(displayed_contents): 

1848 is_last_item = (i == len(displayed_contents) - 1) and ( 

1849 remaining_count == 0 

1850 ) 

1851 

1852 branch = ( 

1853 "└" + ("─" * (indent_size - 2)) 

1854 if is_last_item 

1855 else "├" + ("─" * (indent_size - 2)) 

1856 ) 

1857 branch += " " 

1858 new_prefix = prefix + ( 

1859 indent if is_last_item else "│" + " " * (indent_size - 1) 

1860 ) 

1861 

1862 name = os.path.basename(item.get("name", "")) 

1863 

1864 if display_size and item.get("type") == "directory": 

1865 sub_contents = self.ls(item.get("name", ""), detail=True) 

1866 num_files = sum( 

1867 1 for sub_item in sub_contents if sub_item.get("type") == "file" 

1868 ) 

1869 num_folders = sum( 

1870 1 

1871 for sub_item in sub_contents 

1872 if sub_item.get("type") == "directory" 

1873 ) 

1874 

1875 if num_files == 0 and num_folders == 0: 

1876 size = " (empty folder)" 

1877 elif num_files == 0: 

1878 size = f" ({num_folders} subfolder{'s' if num_folders > 1 else ''})" 

1879 elif num_folders == 0: 

1880 size = f" ({num_files} file{'s' if num_files > 1 else ''})" 

1881 else: 

1882 size = f" ({num_files} file{'s' if num_files > 1 else ''}, {num_folders} subfolder{'s' if num_folders > 1 else ''})" 

1883 elif display_size and item.get("type") == "file": 

1884 size = f" ({format_bytes(item.get('size', 0))})" 

1885 else: 

1886 size = "" 

1887 

1888 result.append(f"{prefix}{branch}{name}{size}") 

1889 

1890 if item.get("type") == "directory" and recursion_limit > 0: 

1891 result.append( 

1892 self.tree( 

1893 path=item.get("name", ""), 

1894 recursion_limit=recursion_limit - 1, 

1895 max_display=max_display, 

1896 display_size=display_size, 

1897 prefix=new_prefix, 

1898 is_last=is_last_item, 

1899 first=False, 

1900 indent_size=indent_size, 

1901 ) 

1902 ) 

1903 

1904 if remaining_count > 0: 

1905 more_message = f"{remaining_count} more item(s) not displayed." 

1906 result.append( 

1907 f"{prefix}{'└' + ('─' * (indent_size - 2))} {more_message}" 

1908 ) 

1909 

1910 return "\n".join(_ for _ in result if _) 

1911 

1912 # ------------------------------------------------------------------------ 

1913 # Aliases 

1914 

1915 def read_bytes(self, path, start=None, end=None, **kwargs): 

1916 """Alias of `AbstractFileSystem.cat_file`.""" 

1917 return self.cat_file(path, start=start, end=end, **kwargs) 

1918 

1919 def write_bytes(self, path, value, **kwargs): 

1920 """Alias of `AbstractFileSystem.pipe_file`.""" 

1921 self.pipe_file(path, value, **kwargs) 

1922 

1923 def makedir(self, path, create_parents=True, **kwargs): 

1924 """Alias of `AbstractFileSystem.mkdir`.""" 

1925 return self.mkdir(path, create_parents=create_parents, **kwargs) 

1926 

1927 def mkdirs(self, path, exist_ok=False): 

1928 """Alias of `AbstractFileSystem.makedirs`.""" 

1929 return self.makedirs(path, exist_ok=exist_ok) 

1930 

1931 def listdir(self, path, detail=True, **kwargs): 

1932 """Alias of `AbstractFileSystem.ls`.""" 

1933 return self.ls(path, detail=detail, **kwargs) 

1934 

1935 def cp(self, path1, path2, **kwargs): 

1936 """Alias of `AbstractFileSystem.copy`.""" 

1937 return self.copy(path1, path2, **kwargs) 

1938 

1939 def move(self, path1, path2, **kwargs): 

1940 """Alias of `AbstractFileSystem.mv`.""" 

1941 return self.mv(path1, path2, **kwargs) 

1942 

1943 def stat(self, path, **kwargs): 

1944 """Alias of `AbstractFileSystem.info`.""" 

1945 return self.info(path, **kwargs) 

1946 

1947 def disk_usage(self, path, total=True, maxdepth=None, **kwargs): 

1948 """Alias of `AbstractFileSystem.du`.""" 

1949 return self.du(path, total=total, maxdepth=maxdepth, **kwargs) 

1950 

1951 def rename(self, path1, path2, **kwargs): 

1952 """Alias of `AbstractFileSystem.mv`.""" 

1953 return self.mv(path1, path2, **kwargs) 

1954 

1955 def delete(self, path, recursive=False, maxdepth=None): 

1956 """Alias of `AbstractFileSystem.rm`.""" 

1957 return self.rm(path, recursive=recursive, maxdepth=maxdepth) 

1958 

1959 def upload(self, lpath, rpath, recursive=False, **kwargs): 

1960 """Alias of `AbstractFileSystem.put`.""" 

1961 return self.put(lpath, rpath, recursive=recursive, **kwargs) 

1962 

1963 def download(self, rpath, lpath, recursive=False, **kwargs): 

1964 """Alias of `AbstractFileSystem.get`.""" 

1965 return self.get(rpath, lpath, recursive=recursive, **kwargs) 

1966 

1967 def sign(self, path, expiration=100, **kwargs): 

1968 """Create a signed URL representing the given path 

1969 

1970 Some implementations allow temporary URLs to be generated, as a 

1971 way of delegating credentials. 

1972 

1973 Parameters 

1974 ---------- 

1975 path : str 

1976 The path on the filesystem 

1977 expiration : int 

1978 Number of seconds to enable the URL for (if supported) 

1979 

1980 Returns 

1981 ------- 

1982 URL : str 

1983 The signed URL 

1984 

1985 Raises 

1986 ------ 

1987 NotImplementedError : if method is not implemented for a filesystem 

1988 """ 

1989 raise NotImplementedError("Sign is not implemented for this filesystem") 

1990 

1991 def _isfilestore(self): 

1992 # Originally inherited from pyarrow DaskFileSystem. Keeping this 

1993 # here for backwards compatibility as long as pyarrow uses its 

1994 # legacy fsspec-compatible filesystems and thus accepts fsspec 

1995 # filesystems as well 

1996 return False 

1997 

1998 

1999class AbstractBufferedFile(io.IOBase): 

2000 """Convenient class to derive from to provide buffering 

2001 

2002 In the case that the backend does not provide a pythonic file-like object 

2003 already, this class contains much of the logic to build one. The only 

2004 methods that need to be overridden are ``_upload_chunk``, 

2005 ``_initiate_upload`` and ``_fetch_range``. 

2006 """ 

2007 

2008 DEFAULT_BLOCK_SIZE = 5 * 2**20 

2009 _details = None 

2010 

2011 def __init__( 

2012 self, 

2013 fs, 

2014 path, 

2015 mode="rb", 

2016 block_size="default", 

2017 autocommit=True, 

2018 cache_type="readahead", 

2019 cache_options=None, 

2020 size=None, 

2021 **kwargs, 

2022 ): 

2023 """ 

2024 Template for files with buffered reading and writing 

2025 

2026 Parameters 

2027 ---------- 

2028 fs: instance of FileSystem 

2029 path: str 

2030 location in file-system 

2031 mode: str 

2032 Normal file modes. Currently only 'wb', 'ab' or 'rb'. Some file 

2033 systems may be read-only, and some may not support append. 

2034 block_size: int 

2035 Buffer size for reading or writing, 'default' for class default 

2036 autocommit: bool 

2037 Whether to write to final destination; may only impact what 

2038 happens when file is being closed. 

2039 cache_type: {"readahead", "none", "mmap", "bytes"}, default "readahead" 

2040 Caching policy in read mode. See the definitions in ``core``. 

2041 cache_options : dict 

2042 Additional options passed to the constructor for the cache specified 

2043 by `cache_type`. 

2044 size: int 

2045 If given and in read mode, suppressed having to look up the file size 

2046 kwargs: 

2047 Gets stored as self.kwargs 

2048 """ 

2049 from .core import caches 

2050 

2051 self.path = path 

2052 self.fs = fs 

2053 self.mode = mode 

2054 self.blocksize = ( 

2055 self.DEFAULT_BLOCK_SIZE if block_size in ["default", None] else block_size 

2056 ) 

2057 self.loc = 0 

2058 self.autocommit = autocommit 

2059 self.end = None 

2060 self.start = None 

2061 self.closed = False 

2062 

2063 if cache_options is None: 

2064 cache_options = {} 

2065 

2066 if "trim" in kwargs: 

2067 warnings.warn( 

2068 "Passing 'trim' to control the cache behavior has been deprecated. " 

2069 "Specify it within the 'cache_options' argument instead.", 

2070 FutureWarning, 

2071 ) 

2072 cache_options["trim"] = kwargs.pop("trim") 

2073 

2074 self.kwargs = kwargs 

2075 

2076 if mode not in {"ab", "rb", "wb", "xb"}: 

2077 raise NotImplementedError("File mode not supported") 

2078 if mode == "rb": 

2079 if size is not None: 

2080 self.size = size 

2081 else: 

2082 self.size = self.details["size"] 

2083 self.cache = caches[cache_type]( 

2084 self.blocksize, self._fetch_range, self.size, **cache_options 

2085 ) 

2086 else: 

2087 self.buffer = io.BytesIO() 

2088 self.offset = None 

2089 self.forced = False 

2090 self.location = None 

2091 

2092 @property 

2093 def len(self): 

2094 """Total size in bytes, independent of the current file position. 

2095 

2096 Raises AttributeError when the backend has not provided a known size. 

2097 """ 

2098 if self.size is None: 

2099 raise AttributeError("File size is unknown") 

2100 return self.size 

2101 

2102 @property 

2103 def details(self): 

2104 if self._details is None: 

2105 self._details = self.fs.info(self.path) 

2106 return self._details 

2107 

2108 @details.setter 

2109 def details(self, value): 

2110 self._details = value 

2111 self.size = value["size"] 

2112 

2113 @property 

2114 def full_name(self): 

2115 return _unstrip_protocol(self.path, self.fs) 

2116 

2117 @property 

2118 def closed(self): 

2119 # get around this attr being read-only in IOBase 

2120 # use getattr here, since this can be called during del 

2121 return getattr(self, "_closed", True) 

2122 

2123 @closed.setter 

2124 def closed(self, c): 

2125 self._closed = c 

2126 

2127 def __hash__(self): 

2128 if "w" in self.mode: 

2129 return id(self) 

2130 else: 

2131 return int(tokenize(self.details), 16) 

2132 

2133 def __eq__(self, other): 

2134 """Files are equal if they have the same checksum, only in read mode""" 

2135 if self is other: 

2136 return True 

2137 return ( 

2138 isinstance(other, type(self)) 

2139 and self.mode == "rb" 

2140 and other.mode == "rb" 

2141 and hash(self) == hash(other) 

2142 ) 

2143 

2144 def commit(self): 

2145 """Move from temp to final destination""" 

2146 

2147 def discard(self): 

2148 """Throw away temporary file""" 

2149 

2150 def info(self): 

2151 """File information about this path""" 

2152 if self.readable(): 

2153 return self.details 

2154 else: 

2155 raise ValueError("Info not available while writing") 

2156 

2157 def tell(self): 

2158 """Current file location""" 

2159 return self.loc 

2160 

2161 def seek(self, loc, whence=0): 

2162 """Set current file location 

2163 

2164 Parameters 

2165 ---------- 

2166 loc: int 

2167 byte location 

2168 whence: {0, 1, 2} 

2169 from start of file, current location or end of file, resp. 

2170 """ 

2171 loc = int(loc) 

2172 if not self.mode == "rb": 

2173 raise OSError(ESPIPE, "Seek only available in read mode") 

2174 if whence == 0: 

2175 nloc = loc 

2176 elif whence == 1: 

2177 nloc = self.loc + loc 

2178 elif whence == 2: 

2179 nloc = self.size + loc 

2180 else: 

2181 raise ValueError(f"invalid whence ({whence}, should be 0, 1 or 2)") 

2182 if nloc < 0: 

2183 raise ValueError("Seek before start of file") 

2184 self.loc = nloc 

2185 return self.loc 

2186 

2187 def write(self, data): 

2188 """ 

2189 Write data to buffer. 

2190 

2191 Buffer only sent on flush() or if buffer is greater than 

2192 or equal to blocksize. 

2193 

2194 Parameters 

2195 ---------- 

2196 data: bytes 

2197 Set of bytes to be written. 

2198 """ 

2199 if not self.writable(): 

2200 raise ValueError("File not in write mode") 

2201 if self.closed: 

2202 raise ValueError("I/O operation on closed file.") 

2203 if self.forced: 

2204 raise ValueError("This file has been force-flushed, can only close") 

2205 out = self.buffer.write(data) 

2206 self.loc += out 

2207 if self.buffer.tell() >= self.blocksize: 

2208 self.flush() 

2209 return out 

2210 

2211 def flush(self, force=False): 

2212 """ 

2213 Write buffered data to backend store. 

2214 

2215 Writes the current buffer, if it is larger than the block-size, or if 

2216 the file is being closed. 

2217 

2218 Parameters 

2219 ---------- 

2220 force: bool 

2221 When closing, write the last block even if it is smaller than 

2222 blocks are allowed to be. Disallows further writing to this file. 

2223 """ 

2224 

2225 if self.closed: 

2226 raise ValueError("Flush on closed file") 

2227 if force and self.forced: 

2228 raise ValueError("Force flush cannot be called more than once") 

2229 if force: 

2230 self.forced = True 

2231 

2232 if self.readable(): 

2233 # no-op to flush on read-mode 

2234 return 

2235 

2236 if not force and self.buffer.tell() < self.blocksize: 

2237 # Defer write on small block 

2238 return 

2239 

2240 if self.offset is None: 

2241 # Initialize a multipart upload 

2242 self.offset = 0 

2243 try: 

2244 self._initiate_upload() 

2245 except Exception: 

2246 self.closed = True 

2247 raise 

2248 

2249 if self._upload_chunk(final=force) is not False: 

2250 self.offset += self.buffer.seek(0, 2) 

2251 self.buffer = io.BytesIO() 

2252 

2253 def _upload_chunk(self, final=False): 

2254 """Write one part of a multi-block file upload 

2255 

2256 Parameters 

2257 ========== 

2258 final: bool 

2259 This is the last block, so should complete file, if 

2260 self.autocommit is True. 

2261 """ 

2262 # may not yet have been initialized, may need to call _initialize_upload 

2263 

2264 def _initiate_upload(self): 

2265 """Create remote file/upload""" 

2266 pass 

2267 

2268 def _fetch_range(self, start, end): 

2269 """Get the specified set of bytes from remote""" 

2270 return self.fs.cat_file(self.path, start=start, end=end) 

2271 

2272 def read(self, length=-1): 

2273 """ 

2274 Return data from cache, or fetch pieces as necessary 

2275 

2276 Parameters 

2277 ---------- 

2278 length: int (-1) 

2279 Number of bytes to read; if <0, all remaining bytes. 

2280 """ 

2281 length = -1 if length is None else int(length) 

2282 if self.mode != "rb": 

2283 raise ValueError("File not in read mode") 

2284 if length < 0: 

2285 length = self.size - self.loc 

2286 if self.closed: 

2287 raise ValueError("I/O operation on closed file.") 

2288 if length == 0: 

2289 # don't even bother calling fetch 

2290 return b"" 

2291 out = self.cache._fetch(self.loc, self.loc + length) 

2292 

2293 logger.debug( 

2294 "%s read: %i - %i %s", 

2295 self, 

2296 self.loc, 

2297 self.loc + length, 

2298 self.cache._log_stats(), 

2299 ) 

2300 self.loc += len(out) 

2301 return out 

2302 

2303 def readinto(self, b): 

2304 """mirrors builtin file's readinto method 

2305 

2306 https://docs.python.org/3/library/io.html#io.RawIOBase.readinto 

2307 """ 

2308 out = memoryview(b).cast("B") 

2309 if out.readonly: 

2310 self.read(0) 

2311 raise TypeError("readinto() argument must be read-write bytes-like object") 

2312 data = self.read(out.nbytes) 

2313 out[: len(data)] = data 

2314 return len(data) 

2315 

2316 def readuntil(self, char=b"\n", blocks=None): 

2317 """Return data between current position and first occurrence of char 

2318 

2319 char is included in the output, except if the end of the tile is 

2320 encountered first. 

2321 

2322 Parameters 

2323 ---------- 

2324 char: bytes 

2325 Thing to find 

2326 blocks: None or int 

2327 How much to read in each go. Defaults to file blocksize - which may 

2328 mean a new read on every call. 

2329 """ 

2330 out = [] 

2331 tail = b"" 

2332 overlap = max(len(char) - 1, 0) 

2333 while True: 

2334 start = self.tell() 

2335 part = self.read(blocks or self.blocksize) 

2336 if len(part) == 0: 

2337 break 

2338 data = tail + part 

2339 found = data.find(char) 

2340 if found > -1: 

2341 consumed = found + len(char) - len(tail) 

2342 out.append(part[:consumed]) 

2343 self.seek(start + consumed) 

2344 break 

2345 out.append(part) 

2346 tail = data[-overlap:] if overlap else b"" 

2347 return b"".join(out) 

2348 

2349 def readline(self): 

2350 """Read until and including the first occurrence of newline character 

2351 

2352 Note that, because of character encoding, this is not necessarily a 

2353 true line ending. 

2354 """ 

2355 return self.readuntil(b"\n") 

2356 

2357 def __next__(self): 

2358 out = self.readline() 

2359 if out: 

2360 return out 

2361 raise StopIteration 

2362 

2363 def __iter__(self): 

2364 return self 

2365 

2366 def readlines(self): 

2367 """Return all data, split by the newline character, including the newline character""" 

2368 data = self.read() 

2369 if not data: 

2370 return [] 

2371 lines = data.split(b"\n") 

2372 out = [l + b"\n" for l in lines[:-1]] 

2373 if data.endswith(b"\n"): 

2374 return out 

2375 else: 

2376 return out + [lines[-1]] 

2377 # return list(self) ??? 

2378 

2379 def readinto1(self, b): 

2380 return self.readinto(b) 

2381 

2382 def close(self): 

2383 """Close file 

2384 

2385 Finalizes writes, discards cache 

2386 """ 

2387 if getattr(self, "_unclosable", False): 

2388 return 

2389 if self.closed: 

2390 return 

2391 try: 

2392 if self.mode == "rb": 

2393 cache = getattr(self, "cache", None) 

2394 if cache is not None: 

2395 close = getattr(cache, "close", None) 

2396 if callable(close): 

2397 close() 

2398 self.cache = None 

2399 else: 

2400 if not getattr(self, "forced", True): 

2401 self.flush(force=True) 

2402 

2403 if self.fs is not None: 

2404 self.fs.invalidate_cache(self.path) 

2405 self.fs.invalidate_cache(self.fs._parent(self.path)) 

2406 finally: 

2407 self.closed = True 

2408 

2409 def readable(self): 

2410 """Whether opened for reading""" 

2411 return "r" in self.mode and not self.closed 

2412 

2413 def seekable(self): 

2414 """Whether is seekable (only in read mode)""" 

2415 return self.readable() 

2416 

2417 def writable(self): 

2418 """Whether opened for writing""" 

2419 return self.mode in {"wb", "ab", "xb"} and not self.closed 

2420 

2421 def __reduce__(self): 

2422 if self.mode != "rb": 

2423 raise RuntimeError("Pickling a writeable file is not supported") 

2424 

2425 return reopen, ( 

2426 self.fs, 

2427 self.path, 

2428 self.mode, 

2429 self.blocksize, 

2430 self.loc, 

2431 self.size, 

2432 self.autocommit, 

2433 self.cache.name if self.cache else "none", 

2434 self.kwargs, 

2435 ) 

2436 

2437 def __del__(self): 

2438 if not self.closed: 

2439 self.close() 

2440 

2441 def __str__(self): 

2442 return f"<File-like object {type(self.fs).__name__}, {self.path}>" 

2443 

2444 __repr__ = __str__ 

2445 

2446 def __enter__(self): 

2447 return self 

2448 

2449 def __exit__(self, *args): 

2450 self.close() 

2451 

2452 

2453def reopen(fs, path, mode, blocksize, loc, size, autocommit, cache_type, kwargs): 

2454 file = fs.open( 

2455 path, 

2456 mode=mode, 

2457 block_size=blocksize, 

2458 autocommit=autocommit, 

2459 cache_type=cache_type, 

2460 size=size, 

2461 **kwargs, 

2462 ) 

2463 if loc > 0: 

2464 file.seek(loc) 

2465 return file