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

923 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, ClassVar 

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 glob_translate, 

22 isfilelike, 

23 other_paths, 

24 read_block, 

25 stringify_path, 

26 tokenize, 

27) 

28 

29logger = logging.getLogger("fsspec") 

30 

31 

32def make_instance(cls, args, kwargs): 

33 return cls(*args, **kwargs) 

34 

35 

36FORK_AVAILABLE = hasattr(os, "register_at_fork") 

37 

38 

39if FORK_AVAILABLE: 

40 _registered_classes = weakref.WeakSet() 

41 

42 def _reset_instances_lock(): 

43 for cls in _registered_classes: 

44 cls._instantiation_lock = threading.RLock() 

45 cls._cache.clear() 

46 cls._pid = os.getpid() 

47 

48 os.register_at_fork(after_in_child=_reset_instances_lock) 

49 

50 

51class _Cached(type): 

52 """ 

53 Metaclass for caching file system instances. 

54 

55 Notes 

56 ----- 

57 Instances are cached according to 

58 

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

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

61 

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

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

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

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

66 """ 

67 

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

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

70 

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

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

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

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

75 # debug option for analysing fork/spawn conditions 

76 cls._cache = weakref.WeakValueDictionary() 

77 else: 

78 cls._cache = {} 

79 cls._pid = os.getpid() 

80 cls._instantiation_lock = threading.RLock() 

81 

82 if FORK_AVAILABLE: 

83 _registered_classes.add(cls) 

84 

85 def _check_instance_cache(cls, token): 

86 inst = cls._cache.get(token) 

87 if inst is not None: 

88 cls._latest = token 

89 return inst 

90 

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

92 kwargs = apply_config(cls, kwargs) 

93 extra_tokens = tuple( 

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

95 ) 

96 strip_tokenize_options = { 

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

98 } 

99 pid = os.getpid() 

100 

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

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

103 else: 

104 token = tokenize( 

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

106 ) 

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

108 

109 if pid != cls._pid: 

110 with cls._instantiation_lock: 

111 if pid != cls._pid: 

112 cls._cache.clear() 

113 cls._pid = pid 

114 

115 if not skip and cls.cachable: 

116 inst = cls._check_instance_cache(token) 

117 if inst is not None: 

118 return inst 

119 

120 with cls._instantiation_lock: 

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

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

123 inst = cls._check_instance_cache(token) 

124 if inst is not None: 

125 return inst 

126 

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

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

129 obj._fs_token_ = token 

130 obj.storage_args = args 

131 obj.storage_options = kwargs 

132 if obj.async_impl and obj.mirror_sync_methods: 

133 from .asyn import mirror_sync_methods 

134 

135 mirror_sync_methods(obj) 

136 

137 if cls.cachable and not skip: 

138 with cls._instantiation_lock: 

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

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

141 inst = cls._check_instance_cache(token) 

142 if inst is not None: 

143 return inst 

144 

145 cls._latest = token 

146 cls._cache[token] = obj 

147 return obj 

148 

149 

150class AbstractFileSystem(metaclass=_Cached): 

151 """ 

152 An abstract super-class for pythonic file-systems 

153 

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

155 from here. 

156 """ 

157 

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

159 _cached = False 

160 blocksize = 2**22 

161 sep = "/" 

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

163 _latest = None 

164 async_impl = False 

165 mirror_sync_methods = False 

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

167 transaction_type = Transaction 

168 

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

170 _extra_tokenize_attributes = () 

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

172 _strip_tokenize_options = () 

173 

174 # Set by _Cached metaclass 

175 storage_args: tuple[Any, ...] 

176 storage_options: dict[str, Any] 

177 

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

179 """Create and configure file-system instance 

180 

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

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

183 implementations to cache instances if they wish. 

184 

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

186 

187 Subclasses should call this method. 

188 

189 Parameters 

190 ---------- 

191 use_listings_cache, listings_expiry_time, max_paths: 

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

193 directory listing caching. Pass use_listings_cache=False 

194 to disable such caching. 

195 skip_instance_cache: bool 

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

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

198 storing this instance. 

199 asynchronous: bool 

200 loop: asyncio-compatible IOLoop or None 

201 """ 

202 if self._cached: 

203 # reusing instance, don't change 

204 return 

205 self._cached = True 

206 self._intrans = False 

207 self._transaction = None 

208 self._invalidated_caches_in_transaction = [] 

209 self.dircache = DirCache(**storage_options) 

210 

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

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

213 

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

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

216 # This is set in _Cached 

217 self._fs_token_ = None 

218 

219 @property 

220 def fsid(self): 

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

222 across sessions. 

223 """ 

224 raise NotImplementedError 

225 

226 @property 

227 def _fs_token(self): 

228 return self._fs_token_ 

229 

230 def __dask_tokenize__(self): 

231 return self._fs_token 

232 

233 def __hash__(self): 

234 return int(self._fs_token, 16) 

235 

236 def __eq__(self, other): 

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

238 

239 def __reduce__(self): 

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

241 

242 @classmethod 

243 def _strip_protocol(cls, path): 

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

245 

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

247 """ 

248 if isinstance(path, list): 

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

250 path = stringify_path(path) 

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

252 for protocol in protos: 

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

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

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

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

257 path = path.rstrip("/") 

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

259 return path or cls.root_marker 

260 

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

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

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

264 for protocol in protos: 

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

266 return name 

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

268 

269 @staticmethod 

270 def _get_kwargs_from_urls(path): 

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

272 

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

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

275 

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

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

278 """ 

279 # by default, nothing happens 

280 return {} 

281 

282 @classmethod 

283 def current(cls): 

284 """Return the most recently instantiated FileSystem 

285 

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

287 """ 

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

289 if inst is not None: 

290 return inst 

291 return cls() 

292 

293 @property 

294 def transaction(self): 

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

296 

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

298 for the normal and exception cases. 

299 """ 

300 if self._transaction is None: 

301 self._transaction = self.transaction_type(self) 

302 return self._transaction 

303 

304 def start_transaction(self): 

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

306 self._intrans = True 

307 self._transaction = self.transaction_type(self) 

308 return self.transaction 

309 

310 def end_transaction(self): 

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

312 self.transaction.complete() 

313 self._transaction = None 

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

315 for path in self._invalidated_caches_in_transaction: 

316 self.invalidate_cache(path) 

317 self._invalidated_caches_in_transaction.clear() 

318 

319 def invalidate_cache(self, path=None): 

320 """ 

321 Discard any cached directory information 

322 

323 Parameters 

324 ---------- 

325 path: string or None 

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

327 path. 

328 """ 

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

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

331 # subclass to ensure expiring caches after transacations correctly. 

332 # See the implementation of FTPFileSystem in ftp.py 

333 if self._intrans: 

334 self._invalidated_caches_in_transaction.append(path) 

335 

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

337 """ 

338 Create directory entry at path 

339 

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

341 this instance only and not touch the real filesystem 

342 

343 Parameters 

344 ---------- 

345 path: str 

346 location 

347 create_parents: bool 

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

349 kwargs: 

350 may be permissions, etc. 

351 """ 

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

353 

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

355 """Recursively make directories 

356 

357 Creates directory at path and any intervening required directories. 

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

359 file. 

360 

361 Parameters 

362 ---------- 

363 path: str 

364 leaf directory name 

365 exist_ok: bool (False) 

366 If False, will error if the target already exists 

367 """ 

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

369 

370 def rmdir(self, path): 

371 """Remove a directory, if empty""" 

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

373 

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

375 """List objects at path. 

376 

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

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

379 are requested. 

380 

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

382 but must be consistent across implementations. 

383 Must include: 

384 

385 - full path to the entry (without protocol) 

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

387 be ``None``. 

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

389 

390 Additional information 

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

392 checksum, etc. 

393 

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

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

396 common where listing may be expensive. 

397 

398 Parameters 

399 ---------- 

400 path: str 

401 detail: bool 

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

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

404 (str). 

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

406 information 

407 

408 Returns 

409 ------- 

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

411 dicts if detail is True. 

412 """ 

413 raise NotImplementedError 

414 

415 def _ls_from_cache(self, path): 

416 """Check cache for listing 

417 

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

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

420 """ 

421 parent = self._parent(path) 

422 try: 

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

424 except KeyError: 

425 pass 

426 try: 

427 files = [ 

428 f 

429 for f in self.dircache[parent] 

430 if f["name"] == path 

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

432 ] 

433 if len(files) == 0: 

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

435 raise FileNotFoundError(path) 

436 return files 

437 except KeyError: 

438 pass 

439 

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

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

442 

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

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

445 

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

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

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

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

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

451 it resumes walk() again. 

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

453 

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

455 a directory, such as links. 

456 

457 Parameters 

458 ---------- 

459 path: str 

460 Root to recurse into 

461 maxdepth: int 

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

463 on link-based file-systems. 

464 topdown: bool (True) 

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

466 the bottom upwards. 

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

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

469 If raise, an underlying exception will be raised; 

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

471 kwargs: passed to ``ls`` 

472 """ 

473 if maxdepth is not None and maxdepth < 1: 

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

475 

476 path = self._strip_protocol(path) 

477 full_dirs = {} 

478 dirs = {} 

479 files = {} 

480 

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

482 try: 

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

484 except (FileNotFoundError, OSError) as e: 

485 if on_error == "raise": 

486 raise 

487 if callable(on_error): 

488 on_error(e) 

489 return 

490 

491 for info in listing: 

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

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

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

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

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

497 # do not include "self" path 

498 full_dirs[name] = pathname 

499 dirs[name] = info 

500 elif pathname == path: 

501 # file-like with same name as give path 

502 files[""] = info 

503 else: 

504 files[name] = info 

505 

506 if not detail: 

507 dirs = list(dirs) 

508 files = list(files) 

509 

510 if topdown: 

511 # Yield before recursion if walking top down 

512 yield path, dirs, files 

513 

514 if maxdepth is not None: 

515 maxdepth -= 1 

516 if maxdepth < 1: 

517 if not topdown: 

518 yield path, dirs, files 

519 return 

520 

521 for d in dirs: 

522 yield from self.walk( 

523 full_dirs[d], 

524 maxdepth=maxdepth, 

525 detail=detail, 

526 topdown=topdown, 

527 **kwargs, 

528 ) 

529 

530 if not topdown: 

531 # Yield after recursion if walking bottom up 

532 yield path, dirs, files 

533 

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

535 """List all files below path. 

536 

537 Like posix ``find`` command without conditions 

538 

539 Parameters 

540 ---------- 

541 path : str 

542 maxdepth: int or None 

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

544 withdirs: bool 

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

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

547 kwargs are passed to ``ls``. 

548 """ 

549 # TODO: allow equivalent of -name parameter 

550 path = self._strip_protocol(path) 

551 out = {} 

552 

553 # Add the root directory if withdirs is requested 

554 # This is needed for posix glob compliance 

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

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

557 

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

559 if withdirs: 

560 files.update(dirs) 

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

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

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

564 # when path happens to be a file 

565 out[path] = {} 

566 names = sorted(out) 

567 if not detail: 

568 return names 

569 else: 

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

571 

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

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

574 

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

576 

577 Parameters 

578 ---------- 

579 path: str 

580 total: bool 

581 Whether to sum all the file sizes 

582 maxdepth: int or None 

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

584 withdirs: bool 

585 Whether to include directory paths in the output. 

586 kwargs: passed to ``find`` 

587 

588 Returns 

589 ------- 

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

591 refer to bytes used. 

592 """ 

593 sizes = {} 

594 if withdirs and self.isdir(path): 

595 # Include top-level directory in output 

596 info = self.info(path) 

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

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

599 info = self.info(f) 

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

601 if total: 

602 return sum(sizes.values()) 

603 else: 

604 return sizes 

605 

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

607 """Find files by glob-matching. 

608 

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

610 

611 Parameters 

612 ---------- 

613 path: str 

614 The glob pattern to match against 

615 maxdepth: int or None 

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

617 Must be at least 1 if provided. 

618 kwargs: 

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

620 

621 Returns 

622 ------- 

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

624 

625 Notes 

626 ----- 

627 Supported patterns: 

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

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

630 - '?': Matches exactly one character 

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

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

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

634 

635 Special behaviors: 

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

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

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

639 - Special characters in character classes are escaped properly 

640 

641 Limitations: 

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

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

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

645 """ 

646 if maxdepth is not None and maxdepth < 1: 

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

648 

649 import re 

650 

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

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

653 path = self._strip_protocol(path) 

654 append_slash_to_dirname = ends_with_sep or path.endswith( 

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

656 ) 

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

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

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

660 

661 min_idx = min(idx_star, idx_qmark, idx_brace) 

662 

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

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

665 

666 if not has_magic(path): 

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

668 if not detail: 

669 return [path] 

670 else: 

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

672 else: 

673 if not detail: 

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

675 else: 

676 return {} 

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

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

679 root = path[: min_idx + 1] 

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

681 else: 

682 root = "" 

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

684 

685 if "**" in path: 

686 if maxdepth is not None: 

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

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

689 depth = depth - depth_double_stars + maxdepth 

690 else: 

691 depth = None 

692 

693 allpaths = self.find( 

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

695 ) 

696 

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

698 pattern = re.compile(pattern) 

699 

700 out = { 

701 p: info 

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

703 if pattern.match( 

704 p + "/" 

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

706 else p 

707 ) 

708 } 

709 

710 if detail: 

711 return out 

712 else: 

713 return list(out) 

714 

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

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

717 try: 

718 self.info(path, **kwargs) 

719 return True 

720 except: # noqa: E722 

721 # any exception allowed bar FileNotFoundError? 

722 return False 

723 

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

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

726 broken links)""" 

727 return self.exists(path) 

728 

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

730 """Give details of entry at path 

731 

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

733 would with ``detail=True``. 

734 

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

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

737 

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

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

740 

741 Returns 

742 ------- 

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

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

745 """ 

746 path = self._strip_protocol(path) 

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

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

749 if out: 

750 return out[0] 

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

752 path = path.rstrip("/") 

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

754 if len(out1) == 1: 

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

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

757 return out1[0] 

758 elif len(out1) > 1 or out: 

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

760 else: 

761 raise FileNotFoundError(path) 

762 

763 def checksum(self, path): 

764 """Unique value for current version of file 

765 

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

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

768 *might* have changed. 

769 

770 This should normally be overridden; default will probably capture 

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

772 access timestamp (which would be bad) 

773 """ 

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

775 

776 def size(self, path): 

777 """Size in bytes of file""" 

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

779 

780 def sizes(self, paths): 

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

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

783 

784 def isdir(self, path): 

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

786 try: 

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

788 except OSError: 

789 return False 

790 

791 def isfile(self, path): 

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

793 try: 

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

795 except: # noqa: E722 

796 return False 

797 

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

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

800 

801 Parameters 

802 ---------- 

803 path: str 

804 URL of file on this filesystems 

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

806 """ 

807 with self.open( 

808 path, 

809 mode="r", 

810 encoding=encoding, 

811 errors=errors, 

812 newline=newline, 

813 **kwargs, 

814 ) as f: 

815 return f.read() 

816 

817 def write_text( 

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

819 ): 

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

821 

822 An existing file will be overwritten. 

823 

824 Parameters 

825 ---------- 

826 path: str 

827 URL of file on this filesystems 

828 value: str 

829 Text to write. 

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

831 """ 

832 with self.open( 

833 path, 

834 mode="w", 

835 encoding=encoding, 

836 errors=errors, 

837 newline=newline, 

838 **kwargs, 

839 ) as f: 

840 return f.write(value) 

841 

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

843 """Get the content of a file 

844 

845 Parameters 

846 ---------- 

847 path: URL of file on this filesystems 

848 start, end: int 

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

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

851 end of file, respectively 

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

853 """ 

854 # explicitly set buffering off? 

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

856 if start is not None: 

857 if start >= 0: 

858 f.seek(start) 

859 else: 

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

861 if end is not None: 

862 if end < 0: 

863 end = f.size + end 

864 return f.read(end - f.tell()) 

865 return f.read() 

866 

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

868 """Set the bytes of given file""" 

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

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

871 # not as well supported 

872 raise FileExistsError 

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

874 f.write(value) 

875 

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

877 """Put value into path 

878 

879 (counterpart to ``cat``) 

880 

881 Parameters 

882 ---------- 

883 path: string or dict(str, bytes) 

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

885 a mapping of {path: bytesvalue}. 

886 value: bytes, optional 

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

888 ``path`` is a dict 

889 """ 

890 if isinstance(path, str): 

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

892 elif isinstance(path, dict): 

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

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

895 else: 

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

897 

898 def cat_ranges( 

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

900 ): 

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

902 

903 Parameters 

904 ---------- 

905 paths: list 

906 A list of of filepaths on this filesystems 

907 starts, ends: int or list 

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

909 used to read all the specified files. 

910 """ 

911 if max_gap is not None: 

912 raise NotImplementedError 

913 if not isinstance(paths, list): 

914 raise TypeError 

915 if not isinstance(starts, list): 

916 starts = [starts] * len(paths) 

917 if not isinstance(ends, list): 

918 ends = [ends] * len(paths) 

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

920 raise ValueError 

921 out = [] 

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

923 try: 

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

925 except Exception as e: 

926 if on_error == "return": 

927 out.append(e) 

928 else: 

929 raise 

930 return out 

931 

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

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

934 

935 Parameters 

936 ---------- 

937 recursive: bool 

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

939 contained files 

940 on_error : "raise", "omit", "return" 

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

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

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

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

945 instance. 

946 kwargs: passed to cat_file 

947 

948 Returns 

949 ------- 

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

951 or the path has been otherwise expanded 

952 """ 

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

954 if ( 

955 len(paths) > 1 

956 or isinstance(path, list) 

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

958 ): 

959 out = {} 

960 for path in paths: 

961 try: 

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

963 except Exception as e: 

964 if on_error == "raise": 

965 raise 

966 if on_error == "return": 

967 out[path] = e 

968 return out 

969 else: 

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

971 

972 def get_file(self, rpath, lpath, callback=DEFAULT_CALLBACK, outfile=None, **kwargs): 

973 """Copy single remote file to local""" 

974 from .implementations.local import LocalFileSystem 

975 

976 if isfilelike(lpath): 

977 outfile = lpath 

978 elif self.isdir(rpath): 

979 os.makedirs(lpath, exist_ok=True) 

980 return None 

981 

982 fs = LocalFileSystem(auto_mkdir=True) 

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

984 

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

986 if outfile is None: 

987 outfile = open(lpath, "wb") 

988 

989 try: 

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

991 data = True 

992 while data: 

993 data = f1.read(self.blocksize) 

994 segment_len = outfile.write(data) 

995 if segment_len is None: 

996 segment_len = len(data) 

997 callback.relative_update(segment_len) 

998 finally: 

999 if not isfilelike(lpath): 

1000 outfile.close() 

1001 

1002 def get( 

1003 self, 

1004 rpath, 

1005 lpath, 

1006 recursive=False, 

1007 callback=DEFAULT_CALLBACK, 

1008 maxdepth=None, 

1009 **kwargs, 

1010 ): 

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

1012 

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

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

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

1016 and will be expanded. 

1017 

1018 Calls get_file for each source. 

1019 """ 

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

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

1022 # are provided as lists 

1023 rpaths = rpath 

1024 lpaths = lpath 

1025 else: 

1026 from .implementations.local import ( 

1027 LocalFileSystem, 

1028 make_path_posix, 

1029 trailing_sep, 

1030 ) 

1031 

1032 source_is_str = isinstance(rpath, str) 

1033 rpaths = self.expand_path( 

1034 rpath, recursive=recursive, maxdepth=maxdepth, **kwargs 

1035 ) 

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

1037 # Non-recursive glob does not copy directories 

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

1039 if not rpaths: 

1040 return 

1041 

1042 if isinstance(lpath, str): 

1043 lpath = make_path_posix(lpath) 

1044 

1045 source_is_file = len(rpaths) == 1 

1046 dest_is_dir = isinstance(lpath, str) and ( 

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

1048 ) 

1049 

1050 exists = source_is_str and ( 

1051 (has_magic(rpath) and source_is_file) 

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

1053 ) 

1054 lpaths = other_paths( 

1055 rpaths, 

1056 lpath, 

1057 exists=exists, 

1058 flatten=not source_is_str, 

1059 ) 

1060 

1061 callback.set_size(len(lpaths)) 

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

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

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

1065 

1066 def put_file( 

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

1068 ): 

1069 """Copy single file to remote""" 

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

1071 raise FileExistsError 

1072 if os.path.isdir(lpath): 

1073 self.makedirs(rpath, exist_ok=True) 

1074 return None 

1075 

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

1077 size = f1.seek(0, 2) 

1078 callback.set_size(size) 

1079 f1.seek(0) 

1080 

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

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

1083 while f1.tell() < size: 

1084 data = f1.read(self.blocksize) 

1085 segment_len = f2.write(data) 

1086 if segment_len is None: 

1087 segment_len = len(data) 

1088 callback.relative_update(segment_len) 

1089 

1090 def put( 

1091 self, 

1092 lpath, 

1093 rpath, 

1094 recursive=False, 

1095 callback=DEFAULT_CALLBACK, 

1096 maxdepth=None, 

1097 **kwargs, 

1098 ): 

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

1100 

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

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

1103 will go within. 

1104 

1105 Calls put_file for each source. 

1106 """ 

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

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

1109 # are provided as lists 

1110 rpaths = rpath 

1111 lpaths = lpath 

1112 else: 

1113 from .implementations.local import ( 

1114 LocalFileSystem, 

1115 make_path_posix, 

1116 trailing_sep, 

1117 ) 

1118 

1119 source_is_str = isinstance(lpath, str) 

1120 if source_is_str: 

1121 lpath = make_path_posix(lpath) 

1122 fs = LocalFileSystem() 

1123 lpaths = fs.expand_path( 

1124 lpath, recursive=recursive, maxdepth=maxdepth, **kwargs 

1125 ) 

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

1127 # Non-recursive glob does not copy directories 

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

1129 if not lpaths: 

1130 return 

1131 

1132 source_is_file = len(lpaths) == 1 

1133 dest_is_dir = isinstance(rpath, str) and ( 

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

1135 ) 

1136 

1137 rpath = ( 

1138 self._strip_protocol(rpath) 

1139 if isinstance(rpath, str) 

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

1141 ) 

1142 exists = source_is_str and ( 

1143 (has_magic(lpath) and source_is_file) 

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

1145 ) 

1146 rpaths = other_paths( 

1147 lpaths, 

1148 rpath, 

1149 exists=exists, 

1150 flatten=not source_is_str, 

1151 ) 

1152 

1153 callback.set_size(len(rpaths)) 

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

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

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

1157 

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

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

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

1161 return f.read(size) 

1162 

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

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

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

1166 f.seek(max(-size, -f.size), 2) 

1167 return f.read() 

1168 

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

1170 raise NotImplementedError 

1171 

1172 def copy( 

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

1174 ): 

1175 """Copy within two locations in the filesystem 

1176 

1177 on_error : "raise", "ignore" 

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

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

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

1181 """ 

1182 if on_error is None and recursive: 

1183 on_error = "ignore" 

1184 elif on_error is None: 

1185 on_error = "raise" 

1186 

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

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

1189 # are provided as lists 

1190 paths1 = path1 

1191 paths2 = path2 

1192 else: 

1193 from .implementations.local import trailing_sep 

1194 

1195 source_is_str = isinstance(path1, str) 

1196 paths1 = self.expand_path( 

1197 path1, recursive=recursive, maxdepth=maxdepth, **kwargs 

1198 ) 

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

1200 # Non-recursive glob does not copy directories 

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

1202 if not paths1: 

1203 return 

1204 

1205 source_is_file = len(paths1) == 1 

1206 dest_is_dir = isinstance(path2, str) and ( 

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

1208 ) 

1209 

1210 exists = source_is_str and ( 

1211 (has_magic(path1) and source_is_file) 

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

1213 ) 

1214 paths2 = other_paths( 

1215 paths1, 

1216 path2, 

1217 exists=exists, 

1218 flatten=not source_is_str, 

1219 ) 

1220 

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

1222 try: 

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

1224 except FileNotFoundError: 

1225 if on_error == "raise": 

1226 raise 

1227 

1228 def expand_path( 

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

1230 ): 

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

1232 to files or directories. 

1233 

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

1235 """ 

1236 

1237 if maxdepth is not None and maxdepth < 1: 

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

1239 

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

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

1242 else: 

1243 out = set() 

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

1245 for p in path: 

1246 if not assume_literal and has_magic(p): 

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

1248 out |= bit 

1249 if recursive: 

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

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

1252 # after decrementing then avoid expand_path call. 

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

1254 continue 

1255 out |= set( 

1256 self.expand_path( 

1257 list(bit), 

1258 recursive=recursive, 

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

1260 assume_literal=True, 

1261 **kwargs, 

1262 ) 

1263 ) 

1264 continue 

1265 elif recursive: 

1266 rec = set( 

1267 self.find( 

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

1269 ) 

1270 ) 

1271 out |= rec 

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

1273 # should only check once, for the root 

1274 out.add(p) 

1275 if not out: 

1276 raise FileNotFoundError(path) 

1277 return sorted(out) 

1278 

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

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

1281 if path1 == path2: 

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

1283 else: 

1284 # explicitly raise exception to prevent data corruption 

1285 self.copy( 

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

1287 ) 

1288 self.rm(path1, recursive=recursive) 

1289 

1290 def rm_file(self, path): 

1291 """Delete a file""" 

1292 self._rm(path) 

1293 

1294 def _rm(self, path): 

1295 """Delete one file""" 

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

1297 raise NotImplementedError 

1298 

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

1300 """Delete files. 

1301 

1302 Parameters 

1303 ---------- 

1304 path: str or list of str 

1305 File(s) to delete. 

1306 recursive: bool 

1307 If file(s) are directories, recursively delete contents and then 

1308 also remove the directory 

1309 maxdepth: int or None 

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

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

1312 possible. 

1313 """ 

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

1315 for p in reversed(path): 

1316 self.rm_file(p) 

1317 

1318 @classmethod 

1319 def _parent(cls, path): 

1320 path = cls._strip_protocol(path) 

1321 if "/" in path: 

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

1323 return cls.root_marker + parent 

1324 else: 

1325 return cls.root_marker 

1326 

1327 def _open( 

1328 self, 

1329 path, 

1330 mode="rb", 

1331 block_size=None, 

1332 autocommit=True, 

1333 cache_options=None, 

1334 **kwargs, 

1335 ): 

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

1337 return AbstractBufferedFile( 

1338 self, 

1339 path, 

1340 mode, 

1341 block_size, 

1342 autocommit, 

1343 cache_options=cache_options, 

1344 **kwargs, 

1345 ) 

1346 

1347 def open( 

1348 self, 

1349 path, 

1350 mode="rb", 

1351 block_size=None, 

1352 cache_options=None, 

1353 compression=None, 

1354 **kwargs, 

1355 ): 

1356 """ 

1357 Return a file-like object from the filesystem 

1358 

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

1360 block. 

1361 

1362 Parameters 

1363 ---------- 

1364 path: str 

1365 Target file 

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

1367 See builtin ``open()`` 

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

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

1370 atomic is implementation-dependent. 

1371 block_size: int 

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

1373 cache_options : dict, optional 

1374 Extra arguments to pass through to the cache. 

1375 compression: string or None 

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

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

1378 compression from the filename suffix. 

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

1380 """ 

1381 import io 

1382 

1383 path = self._strip_protocol(path) 

1384 if "b" not in mode: 

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

1386 

1387 text_kwargs = { 

1388 k: kwargs.pop(k) 

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

1390 if k in kwargs 

1391 } 

1392 return io.TextIOWrapper( 

1393 self.open( 

1394 path, 

1395 mode, 

1396 block_size=block_size, 

1397 cache_options=cache_options, 

1398 compression=compression, 

1399 **kwargs, 

1400 ), 

1401 **text_kwargs, 

1402 ) 

1403 else: 

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

1405 f = self._open( 

1406 path, 

1407 mode=mode, 

1408 block_size=block_size, 

1409 autocommit=ac, 

1410 cache_options=cache_options, 

1411 **kwargs, 

1412 ) 

1413 if compression is not None: 

1414 from fsspec.compression import compr 

1415 from fsspec.core import get_compression 

1416 

1417 compression = get_compression(path, compression) 

1418 compress = compr[compression] 

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

1420 

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

1422 self.transaction.files.append(f) 

1423 return f 

1424 

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

1426 """Create empty file, or update timestamp 

1427 

1428 Parameters 

1429 ---------- 

1430 path: str 

1431 file location 

1432 truncate: bool 

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

1434 leave file unchanged, if backend allows this 

1435 """ 

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

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

1438 pass 

1439 else: 

1440 raise NotImplementedError # update timestamp, if possible 

1441 

1442 def ukey(self, path): 

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

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

1445 

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

1447 """Read a block of bytes from 

1448 

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

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

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

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

1453 bytestring returned WILL include the end delimiter string. 

1454 

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

1456 

1457 Parameters 

1458 ---------- 

1459 fn: string 

1460 Path to filename 

1461 offset: int 

1462 Byte offset to start read 

1463 length: int 

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

1465 delimiter: bytes (optional) 

1466 Ensure reading starts and stops at delimiter bytestring 

1467 

1468 Examples 

1469 -------- 

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

1471 b'Alice, 100\\nBo' 

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

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

1474 

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

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

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

1478 

1479 See Also 

1480 -------- 

1481 :func:`fsspec.utils.read_block` 

1482 """ 

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

1484 size = f.size 

1485 if length is None: 

1486 length = size 

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

1488 length = size - offset 

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

1490 

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

1492 """ 

1493 JSON representation of this filesystem instance. 

1494 

1495 Parameters 

1496 ---------- 

1497 include_password: bool, default True 

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

1499 

1500 Returns 

1501 ------- 

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

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

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

1505 keyword arguments as their own keys. 

1506 

1507 Warnings 

1508 -------- 

1509 Serialized filesystems may contain sensitive information which have been 

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

1511 store and send them in a secure environment! 

1512 """ 

1513 from .json import FilesystemJSONEncoder 

1514 

1515 return json.dumps( 

1516 self, 

1517 cls=type( 

1518 "_FilesystemJSONEncoder", 

1519 (FilesystemJSONEncoder,), 

1520 {"include_password": include_password}, 

1521 ), 

1522 ) 

1523 

1524 @staticmethod 

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

1526 """ 

1527 Recreate a filesystem instance from JSON representation. 

1528 

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

1530 

1531 Parameters 

1532 ---------- 

1533 blob: str 

1534 

1535 Returns 

1536 ------- 

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

1538 

1539 Warnings 

1540 -------- 

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

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

1543 at import time. 

1544 """ 

1545 from .json import FilesystemJSONDecoder 

1546 

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

1548 

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

1550 """ 

1551 JSON-serializable dictionary representation of this filesystem instance. 

1552 

1553 Parameters 

1554 ---------- 

1555 include_password: bool, default True 

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

1557 

1558 Returns 

1559 ------- 

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

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

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

1563 keyword arguments as their own keys. 

1564 

1565 Warnings 

1566 -------- 

1567 Serialized filesystems may contain sensitive information which have been 

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

1569 store and send them in a secure environment! 

1570 """ 

1571 from .json import FilesystemJSONEncoder 

1572 

1573 json_encoder = FilesystemJSONEncoder() 

1574 

1575 cls = type(self) 

1576 proto = self.protocol 

1577 

1578 storage_options = dict(self.storage_options) 

1579 if not include_password: 

1580 storage_options.pop("password", None) 

1581 

1582 return dict( 

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

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

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

1586 **json_encoder.make_serializable(storage_options), 

1587 ) 

1588 

1589 @staticmethod 

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

1591 """ 

1592 Recreate a filesystem instance from dictionary representation. 

1593 

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

1595 

1596 Parameters 

1597 ---------- 

1598 dct: Dict[str, Any] 

1599 

1600 Returns 

1601 ------- 

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

1603 

1604 Warnings 

1605 -------- 

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

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

1608 at import time. 

1609 """ 

1610 from .json import FilesystemJSONDecoder 

1611 

1612 json_decoder = FilesystemJSONDecoder() 

1613 

1614 dct = dict(dct) # Defensive copy 

1615 

1616 cls = FilesystemJSONDecoder.try_resolve_fs_cls(dct) 

1617 if cls is None: 

1618 raise ValueError("Not a serialized AbstractFileSystem") 

1619 

1620 dct.pop("cls", None) 

1621 dct.pop("protocol", None) 

1622 

1623 return cls( 

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

1625 **json_decoder.unmake_serializable(dct), 

1626 ) 

1627 

1628 def _get_pyarrow_filesystem(self): 

1629 """ 

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

1631 """ 

1632 # all instances already also derive from pyarrow 

1633 return self 

1634 

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

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

1637 

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

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

1640 """ 

1641 from .mapping import FSMap 

1642 

1643 return FSMap( 

1644 root, 

1645 self, 

1646 check=check, 

1647 create=create, 

1648 missing_exceptions=missing_exceptions, 

1649 ) 

1650 

1651 @classmethod 

1652 def clear_instance_cache(cls): 

1653 """ 

1654 Clear the cache of filesystem instances. 

1655 

1656 Notes 

1657 ----- 

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

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

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

1661 since the instances refcount will not drop to zero until 

1662 ``clear_instance_cache`` is called. 

1663 """ 

1664 cls._cache.clear() 

1665 

1666 def created(self, path): 

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

1668 raise NotImplementedError 

1669 

1670 def modified(self, path): 

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

1672 raise NotImplementedError 

1673 

1674 def tree( 

1675 self, 

1676 path: str = "/", 

1677 recursion_limit: int = 2, 

1678 max_display: int = 25, 

1679 display_size: bool = False, 

1680 prefix: str = "", 

1681 is_last: bool = True, 

1682 first: bool = True, 

1683 indent_size: int = 4, 

1684 ) -> str: 

1685 """ 

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

1687 

1688 Parameters 

1689 ---------- 

1690 path: Root path to start traversal from 

1691 recursion_limit: Maximum depth of directory traversal 

1692 max_display: Maximum number of items to display per directory 

1693 display_size: Whether to display file sizes 

1694 prefix: Current line prefix for visual tree structure 

1695 is_last: Whether current item is last in its level 

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

1697 indent_size: Number of spaces by indent 

1698 

1699 Returns 

1700 ------- 

1701 str: A string representing the tree structure. 

1702 

1703 Example 

1704 ------- 

1705 >>> from fsspec import filesystem 

1706 

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

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

1709 >>> print(tree) 

1710 """ 

1711 

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

1713 """Format bytes as text.""" 

1714 for prefix, k in ( 

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

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

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

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

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

1720 ): 

1721 if n >= 0.9 * k: 

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

1723 return f"{n}B" 

1724 

1725 result = [] 

1726 

1727 if first: 

1728 result.append(path) 

1729 

1730 if recursion_limit: 

1731 indent = " " * indent_size 

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

1733 contents.sort( 

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

1735 ) 

1736 

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

1738 displayed_contents = contents[:max_display] 

1739 remaining_count = len(contents) - max_display 

1740 else: 

1741 displayed_contents = contents 

1742 remaining_count = 0 

1743 

1744 for i, item in enumerate(displayed_contents): 

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

1746 remaining_count == 0 

1747 ) 

1748 

1749 branch = ( 

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

1751 if is_last_item 

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

1753 ) 

1754 branch += " " 

1755 new_prefix = prefix + ( 

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

1757 ) 

1758 

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

1760 

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

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

1763 num_files = sum( 

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

1765 ) 

1766 num_folders = sum( 

1767 1 

1768 for sub_item in sub_contents 

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

1770 ) 

1771 

1772 if num_files == 0 and num_folders == 0: 

1773 size = " (empty folder)" 

1774 elif num_files == 0: 

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

1776 elif num_folders == 0: 

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

1778 else: 

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

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

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

1782 else: 

1783 size = "" 

1784 

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

1786 

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

1788 result.append( 

1789 self.tree( 

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

1791 recursion_limit=recursion_limit - 1, 

1792 max_display=max_display, 

1793 display_size=display_size, 

1794 prefix=new_prefix, 

1795 is_last=is_last_item, 

1796 first=False, 

1797 indent_size=indent_size, 

1798 ) 

1799 ) 

1800 

1801 if remaining_count > 0: 

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

1803 result.append( 

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

1805 ) 

1806 

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

1808 

1809 # ------------------------------------------------------------------------ 

1810 # Aliases 

1811 

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

1813 """Alias of `AbstractFileSystem.cat_file`.""" 

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

1815 

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

1817 """Alias of `AbstractFileSystem.pipe_file`.""" 

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

1819 

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

1821 """Alias of `AbstractFileSystem.mkdir`.""" 

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

1823 

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

1825 """Alias of `AbstractFileSystem.makedirs`.""" 

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

1827 

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

1829 """Alias of `AbstractFileSystem.ls`.""" 

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

1831 

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

1833 """Alias of `AbstractFileSystem.copy`.""" 

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

1835 

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

1837 """Alias of `AbstractFileSystem.mv`.""" 

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

1839 

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

1841 """Alias of `AbstractFileSystem.info`.""" 

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

1843 

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

1845 """Alias of `AbstractFileSystem.du`.""" 

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

1847 

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

1849 """Alias of `AbstractFileSystem.mv`.""" 

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

1851 

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

1853 """Alias of `AbstractFileSystem.rm`.""" 

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

1855 

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

1857 """Alias of `AbstractFileSystem.put`.""" 

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

1859 

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

1861 """Alias of `AbstractFileSystem.get`.""" 

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

1863 

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

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

1866 

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

1868 way of delegating credentials. 

1869 

1870 Parameters 

1871 ---------- 

1872 path : str 

1873 The path on the filesystem 

1874 expiration : int 

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

1876 

1877 Returns 

1878 ------- 

1879 URL : str 

1880 The signed URL 

1881 

1882 Raises 

1883 ------ 

1884 NotImplementedError : if method is not implemented for a filesystem 

1885 """ 

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

1887 

1888 def _isfilestore(self): 

1889 # Originally inherited from pyarrow DaskFileSystem. Keeping this 

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

1891 # legacy fsspec-compatible filesystems and thus accepts fsspec 

1892 # filesystems as well 

1893 return False 

1894 

1895 

1896class AbstractBufferedFile(io.IOBase): 

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

1898 

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

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

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

1902 ``_initiate_upload`` and ``_fetch_range``. 

1903 """ 

1904 

1905 DEFAULT_BLOCK_SIZE = 5 * 2**20 

1906 _details = None 

1907 

1908 def __init__( 

1909 self, 

1910 fs, 

1911 path, 

1912 mode="rb", 

1913 block_size="default", 

1914 autocommit=True, 

1915 cache_type="readahead", 

1916 cache_options=None, 

1917 size=None, 

1918 **kwargs, 

1919 ): 

1920 """ 

1921 Template for files with buffered reading and writing 

1922 

1923 Parameters 

1924 ---------- 

1925 fs: instance of FileSystem 

1926 path: str 

1927 location in file-system 

1928 mode: str 

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

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

1931 block_size: int 

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

1933 autocommit: bool 

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

1935 happens when file is being closed. 

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

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

1938 cache_options : dict 

1939 Additional options passed to the constructor for the cache specified 

1940 by `cache_type`. 

1941 size: int 

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

1943 kwargs: 

1944 Gets stored as self.kwargs 

1945 """ 

1946 from .core import caches 

1947 

1948 self.path = path 

1949 self.fs = fs 

1950 self.mode = mode 

1951 self.blocksize = ( 

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

1953 ) 

1954 self.loc = 0 

1955 self.autocommit = autocommit 

1956 self.end = None 

1957 self.start = None 

1958 self.closed = False 

1959 

1960 if cache_options is None: 

1961 cache_options = {} 

1962 

1963 if "trim" in kwargs: 

1964 warnings.warn( 

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

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

1967 FutureWarning, 

1968 ) 

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

1970 

1971 self.kwargs = kwargs 

1972 

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

1974 raise NotImplementedError("File mode not supported") 

1975 if mode == "rb": 

1976 if size is not None: 

1977 self.size = size 

1978 else: 

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

1980 self.cache = caches[cache_type]( 

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

1982 ) 

1983 else: 

1984 self.buffer = io.BytesIO() 

1985 self.offset = None 

1986 self.forced = False 

1987 self.location = None 

1988 

1989 @property 

1990 def details(self): 

1991 if self._details is None: 

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

1993 return self._details 

1994 

1995 @details.setter 

1996 def details(self, value): 

1997 self._details = value 

1998 self.size = value["size"] 

1999 

2000 @property 

2001 def full_name(self): 

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

2003 

2004 @property 

2005 def closed(self): 

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

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

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

2009 

2010 @closed.setter 

2011 def closed(self, c): 

2012 self._closed = c 

2013 

2014 def __hash__(self): 

2015 if "w" in self.mode: 

2016 return id(self) 

2017 else: 

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

2019 

2020 def __eq__(self, other): 

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

2022 if self is other: 

2023 return True 

2024 return ( 

2025 isinstance(other, type(self)) 

2026 and self.mode == "rb" 

2027 and other.mode == "rb" 

2028 and hash(self) == hash(other) 

2029 ) 

2030 

2031 def commit(self): 

2032 """Move from temp to final destination""" 

2033 

2034 def discard(self): 

2035 """Throw away temporary file""" 

2036 

2037 def info(self): 

2038 """File information about this path""" 

2039 if self.readable(): 

2040 return self.details 

2041 else: 

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

2043 

2044 def tell(self): 

2045 """Current file location""" 

2046 return self.loc 

2047 

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

2049 """Set current file location 

2050 

2051 Parameters 

2052 ---------- 

2053 loc: int 

2054 byte location 

2055 whence: {0, 1, 2} 

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

2057 """ 

2058 loc = int(loc) 

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

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

2061 if whence == 0: 

2062 nloc = loc 

2063 elif whence == 1: 

2064 nloc = self.loc + loc 

2065 elif whence == 2: 

2066 nloc = self.size + loc 

2067 else: 

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

2069 if nloc < 0: 

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

2071 self.loc = nloc 

2072 return self.loc 

2073 

2074 def write(self, data): 

2075 """ 

2076 Write data to buffer. 

2077 

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

2079 or equal to blocksize. 

2080 

2081 Parameters 

2082 ---------- 

2083 data: bytes 

2084 Set of bytes to be written. 

2085 """ 

2086 if not self.writable(): 

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

2088 if self.closed: 

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

2090 if self.forced: 

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

2092 out = self.buffer.write(data) 

2093 self.loc += out 

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

2095 self.flush() 

2096 return out 

2097 

2098 def flush(self, force=False): 

2099 """ 

2100 Write buffered data to backend store. 

2101 

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

2103 the file is being closed. 

2104 

2105 Parameters 

2106 ---------- 

2107 force: bool 

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

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

2110 """ 

2111 

2112 if self.closed: 

2113 raise ValueError("Flush on closed file") 

2114 if force and self.forced: 

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

2116 if force: 

2117 self.forced = True 

2118 

2119 if self.readable(): 

2120 # no-op to flush on read-mode 

2121 return 

2122 

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

2124 # Defer write on small block 

2125 return 

2126 

2127 if self.offset is None: 

2128 # Initialize a multipart upload 

2129 self.offset = 0 

2130 try: 

2131 self._initiate_upload() 

2132 except Exception: 

2133 self.closed = True 

2134 raise 

2135 

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

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

2138 self.buffer = io.BytesIO() 

2139 

2140 def _upload_chunk(self, final=False): 

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

2142 

2143 Parameters 

2144 ========== 

2145 final: bool 

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

2147 self.autocommit is True. 

2148 """ 

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

2150 

2151 def _initiate_upload(self): 

2152 """Create remote file/upload""" 

2153 pass 

2154 

2155 def _fetch_range(self, start, end): 

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

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

2158 

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

2160 """ 

2161 Return data from cache, or fetch pieces as necessary 

2162 

2163 Parameters 

2164 ---------- 

2165 length: int (-1) 

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

2167 """ 

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

2169 if self.mode != "rb": 

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

2171 if length < 0: 

2172 length = self.size - self.loc 

2173 if self.closed: 

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

2175 if length == 0: 

2176 # don't even bother calling fetch 

2177 return b"" 

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

2179 

2180 logger.debug( 

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

2182 self, 

2183 self.loc, 

2184 self.loc + length, 

2185 self.cache._log_stats(), 

2186 ) 

2187 self.loc += len(out) 

2188 return out 

2189 

2190 def readinto(self, b): 

2191 """mirrors builtin file's readinto method 

2192 

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

2194 """ 

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

2196 data = self.read(out.nbytes) 

2197 out[: len(data)] = data 

2198 return len(data) 

2199 

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

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

2202 

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

2204 encountered first. 

2205 

2206 Parameters 

2207 ---------- 

2208 char: bytes 

2209 Thing to find 

2210 blocks: None or int 

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

2212 mean a new read on every call. 

2213 """ 

2214 out = [] 

2215 while True: 

2216 start = self.tell() 

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

2218 if len(part) == 0: 

2219 break 

2220 found = part.find(char) 

2221 if found > -1: 

2222 out.append(part[: found + len(char)]) 

2223 self.seek(start + found + len(char)) 

2224 break 

2225 out.append(part) 

2226 return b"".join(out) 

2227 

2228 def readline(self): 

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

2230 

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

2232 true line ending. 

2233 """ 

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

2235 

2236 def __next__(self): 

2237 out = self.readline() 

2238 if out: 

2239 return out 

2240 raise StopIteration 

2241 

2242 def __iter__(self): 

2243 return self 

2244 

2245 def readlines(self): 

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

2247 data = self.read() 

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

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

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

2251 return out 

2252 else: 

2253 return out + [lines[-1]] 

2254 # return list(self) ??? 

2255 

2256 def readinto1(self, b): 

2257 return self.readinto(b) 

2258 

2259 def close(self): 

2260 """Close file 

2261 

2262 Finalizes writes, discards cache 

2263 """ 

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

2265 return 

2266 if self.closed: 

2267 return 

2268 try: 

2269 if self.mode == "rb": 

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

2271 if cache is not None: 

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

2273 if callable(close): 

2274 close() 

2275 self.cache = None 

2276 else: 

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

2278 self.flush(force=True) 

2279 

2280 if self.fs is not None: 

2281 self.fs.invalidate_cache(self.path) 

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

2283 finally: 

2284 self.closed = True 

2285 

2286 def readable(self): 

2287 """Whether opened for reading""" 

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

2289 

2290 def seekable(self): 

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

2292 return self.readable() 

2293 

2294 def writable(self): 

2295 """Whether opened for writing""" 

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

2297 

2298 def __reduce__(self): 

2299 if self.mode != "rb": 

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

2301 

2302 return reopen, ( 

2303 self.fs, 

2304 self.path, 

2305 self.mode, 

2306 self.blocksize, 

2307 self.loc, 

2308 self.size, 

2309 self.autocommit, 

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

2311 self.kwargs, 

2312 ) 

2313 

2314 def __del__(self): 

2315 if not self.closed: 

2316 self.close() 

2317 

2318 def __str__(self): 

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

2320 

2321 __repr__ = __str__ 

2322 

2323 def __enter__(self): 

2324 return self 

2325 

2326 def __exit__(self, *args): 

2327 self.close() 

2328 

2329 

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

2331 file = fs.open( 

2332 path, 

2333 mode=mode, 

2334 block_size=blocksize, 

2335 autocommit=autocommit, 

2336 cache_type=cache_type, 

2337 size=size, 

2338 **kwargs, 

2339 ) 

2340 if loc > 0: 

2341 file.seek(loc) 

2342 return file