34 pycompat, |
37 pycompat, |
35 util, |
38 util, |
36 ) |
39 ) |
37 |
40 |
38 if typing.TYPE_CHECKING: |
41 if typing.TYPE_CHECKING: |
|
42 from . import ( |
|
43 ui as uimod, |
|
44 ) |
|
45 |
39 _Tbackgroundfilecloser = TypeVar( |
46 _Tbackgroundfilecloser = TypeVar( |
40 '_Tbackgroundfilecloser', bound='backgroundfilecloser' |
47 '_Tbackgroundfilecloser', bound='backgroundfilecloser' |
41 ) |
48 ) |
42 _Tclosewrapbase = TypeVar('_Tclosewrapbase', bound='closewrapbase') |
49 _Tclosewrapbase = TypeVar('_Tclosewrapbase', bound='closewrapbase') |
43 |
50 _OnErrorFn = Callable[[Exception], Optional[object]] |
44 |
51 |
45 def _avoidambig(path: bytes, oldstat) -> None: |
52 |
|
53 def _avoidambig(path: bytes, oldstat: util.filestat) -> None: |
46 """Avoid file stat ambiguity forcibly |
54 """Avoid file stat ambiguity forcibly |
47 |
55 |
48 This function causes copying ``path`` file, if it is owned by |
56 This function causes copying ``path`` file, if it is owned by |
49 another (see issue5418 and issue5584 for detail). |
57 another (see issue5418 and issue5584 for detail). |
50 """ |
58 """ |
76 @abc.abstractmethod |
84 @abc.abstractmethod |
77 def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs) -> Any: |
85 def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs) -> Any: |
78 ... |
86 ... |
79 |
87 |
80 @abc.abstractmethod |
88 @abc.abstractmethod |
81 def _auditpath(self, path: bytes, mode: bytes) -> Any: |
89 def _auditpath(self, path: bytes, mode: bytes) -> None: |
82 ... |
90 ... |
83 |
91 |
84 @abc.abstractmethod |
92 @abc.abstractmethod |
85 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes: |
93 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes: |
86 ... |
94 ... |
91 return self.read(path) |
99 return self.read(path) |
92 except FileNotFoundError: |
100 except FileNotFoundError: |
93 pass |
101 pass |
94 return b"" |
102 return b"" |
95 |
103 |
96 def tryreadlines(self, path: bytes, mode: bytes = b'rb') -> Any: |
104 def tryreadlines(self, path: bytes, mode: bytes = b'rb') -> List[bytes]: |
97 '''gracefully return an empty array for missing files''' |
105 '''gracefully return an empty array for missing files''' |
98 try: |
106 try: |
99 return self.readlines(path, mode=mode) |
107 return self.readlines(path, mode=mode) |
100 except FileNotFoundError: |
108 except FileNotFoundError: |
101 pass |
109 pass |
113 |
121 |
114 def read(self, path: bytes) -> bytes: |
122 def read(self, path: bytes) -> bytes: |
115 with self(path, b'rb') as fp: |
123 with self(path, b'rb') as fp: |
116 return fp.read() |
124 return fp.read() |
117 |
125 |
118 def readlines(self, path: bytes, mode: bytes = b'rb') -> Any: |
126 def readlines(self, path: bytes, mode: bytes = b'rb') -> List[bytes]: |
119 with self(path, mode=mode) as fp: |
127 with self(path, mode=mode) as fp: |
120 return fp.readlines() |
128 return fp.readlines() |
121 |
129 |
122 def write( |
130 def write( |
123 self, path: bytes, data: bytes, backgroundclose=False, **kwargs |
131 self, path: bytes, data: bytes, backgroundclose: bool = False, **kwargs |
124 ) -> int: |
132 ) -> int: |
125 with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp: |
133 with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp: |
126 return fp.write(data) |
134 return fp.write(data) |
127 |
135 |
128 def writelines( |
136 def writelines( |
129 self, |
137 self, |
130 path: bytes, |
138 path: bytes, |
131 data: Iterable[bytes], |
139 data: Iterable[bytes], |
132 mode: bytes = b'wb', |
140 mode: bytes = b'wb', |
133 notindexed=False, |
141 notindexed: bool = False, |
134 ) -> None: |
142 ) -> None: |
135 with self(path, mode=mode, notindexed=notindexed) as fp: |
143 with self(path, mode=mode, notindexed=notindexed) as fp: |
136 return fp.writelines(data) |
144 return fp.writelines(data) |
137 |
145 |
138 def append(self, path: bytes, data: bytes) -> int: |
146 def append(self, path: bytes, data: bytes) -> int: |
155 return os.path.dirname(path) |
163 return os.path.dirname(path) |
156 |
164 |
157 def exists(self, path: Optional[bytes] = None) -> bool: |
165 def exists(self, path: Optional[bytes] = None) -> bool: |
158 return os.path.exists(self.join(path)) |
166 return os.path.exists(self.join(path)) |
159 |
167 |
160 def fstat(self, fp) -> os.stat_result: |
168 def fstat(self, fp: BinaryIO) -> os.stat_result: |
161 return util.fstat(fp) |
169 return util.fstat(fp) |
162 |
170 |
163 def isdir(self, path: Optional[bytes] = None) -> bool: |
171 def isdir(self, path: Optional[bytes] = None) -> bool: |
164 return os.path.isdir(self.join(path)) |
172 return os.path.isdir(self.join(path)) |
165 |
173 |
247 def makedirs( |
255 def makedirs( |
248 self, path: Optional[bytes] = None, mode: Optional[int] = None |
256 self, path: Optional[bytes] = None, mode: Optional[int] = None |
249 ) -> None: |
257 ) -> None: |
250 return util.makedirs(self.join(path), mode) |
258 return util.makedirs(self.join(path), mode) |
251 |
259 |
252 def makelock(self, info, path: bytes) -> None: |
260 def makelock(self, info: bytes, path: bytes) -> None: |
253 return util.makelock(info, self.join(path)) |
261 return util.makelock(info, self.join(path)) |
254 |
262 |
255 def mkdir(self, path: Optional[bytes] = None) -> None: |
263 def mkdir(self, path: Optional[bytes] = None) -> None: |
256 return os.mkdir(self.join(path)) |
264 return os.mkdir(self.join(path)) |
257 |
265 |
268 if dir: |
276 if dir: |
269 return fd, os.path.join(dir, fname) |
277 return fd, os.path.join(dir, fname) |
270 else: |
278 else: |
271 return fd, fname |
279 return fd, fname |
272 |
280 |
|
281 # TODO: This doesn't match osutil.listdir(). stat=False in pure; |
|
282 # non-optional bool in cext. 'skip' is bool if we trust cext, or bytes |
|
283 # going by how pure uses it. Also, cext returns a custom stat structure. |
|
284 # from cext.osutil.pyi: |
|
285 # |
|
286 # path: bytes, st: bool, skip: Optional[bool] |
273 def readdir( |
287 def readdir( |
274 self, path: Optional[bytes] = None, stat=None, skip=None |
288 self, path: Optional[bytes] = None, stat=None, skip=None |
275 ) -> Any: |
289 ) -> Any: |
276 return util.listdir(self.join(path), stat, skip) |
290 return util.listdir(self.join(path), stat, skip) |
277 |
291 |
278 def readlock(self, path: bytes) -> bytes: |
292 def readlock(self, path: bytes) -> bytes: |
279 return util.readlock(self.join(path)) |
293 return util.readlock(self.join(path)) |
280 |
294 |
281 def rename(self, src: bytes, dst: bytes, checkambig=False) -> None: |
295 def rename(self, src: bytes, dst: bytes, checkambig: bool = False) -> None: |
282 """Rename from src to dst |
296 """Rename from src to dst |
283 |
297 |
284 checkambig argument is used with util.filestat, and is useful |
298 checkambig argument is used with util.filestat, and is useful |
285 only if destination file is guarded by any lock |
299 only if destination file is guarded by any lock |
286 (e.g. repo.lock or repo.wlock). |
300 (e.g. repo.lock or repo.wlock). |
310 def rmdir(self, path: Optional[bytes] = None) -> None: |
324 def rmdir(self, path: Optional[bytes] = None) -> None: |
311 """Remove an empty directory.""" |
325 """Remove an empty directory.""" |
312 return os.rmdir(self.join(path)) |
326 return os.rmdir(self.join(path)) |
313 |
327 |
314 def rmtree( |
328 def rmtree( |
315 self, path: Optional[bytes] = None, ignore_errors=False, forcibly=False |
329 self, |
|
330 path: Optional[bytes] = None, |
|
331 ignore_errors: bool = False, |
|
332 forcibly: bool = False, |
316 ) -> None: |
333 ) -> None: |
317 """Remove a directory tree recursively |
334 """Remove a directory tree recursively |
318 |
335 |
319 If ``forcibly``, this tries to remove READ-ONLY files, too. |
336 If ``forcibly``, this tries to remove READ-ONLY files, too. |
320 """ |
337 """ |
321 if forcibly: |
338 if forcibly: |
322 |
339 |
323 def onexc(function, path, excinfo): |
340 def onexc(function, path: bytes, excinfo): |
324 if function is not os.remove: |
341 if function is not os.remove: |
325 raise |
342 raise |
326 # read-only files cannot be unlinked under Windows |
343 # read-only files cannot be unlinked under Windows |
327 s = os.stat(path) |
344 s = os.stat(path) |
328 if (s.st_mode & stat.S_IWRITE) != 0: |
345 if (s.st_mode & stat.S_IWRITE) != 0: |
355 def tryunlink(self, path: Optional[bytes] = None) -> bool: |
372 def tryunlink(self, path: Optional[bytes] = None) -> bool: |
356 """Attempt to remove a file, ignoring missing file errors.""" |
373 """Attempt to remove a file, ignoring missing file errors.""" |
357 return util.tryunlink(self.join(path)) |
374 return util.tryunlink(self.join(path)) |
358 |
375 |
359 def unlinkpath( |
376 def unlinkpath( |
360 self, path: Optional[bytes] = None, ignoremissing=False, rmdir=True |
377 self, |
|
378 path: Optional[bytes] = None, |
|
379 ignoremissing: bool = False, |
|
380 rmdir: bool = True, |
361 ) -> None: |
381 ) -> None: |
362 return util.unlinkpath( |
382 return util.unlinkpath( |
363 self.join(path), ignoremissing=ignoremissing, rmdir=rmdir |
383 self.join(path), ignoremissing=ignoremissing, rmdir=rmdir |
364 ) |
384 ) |
365 |
385 |
366 def utime(self, path: Optional[bytes] = None, t=None) -> None: |
386 # TODO: could be Tuple[float, float] too. |
|
387 def utime( |
|
388 self, path: Optional[bytes] = None, t: Optional[Tuple[int, int]] = None |
|
389 ) -> None: |
367 return os.utime(self.join(path), t) |
390 return os.utime(self.join(path), t) |
368 |
391 |
369 def walk( |
392 def walk( |
370 self, path: Optional[bytes] = None, onerror=None |
393 self, path: Optional[bytes] = None, onerror: Optional[_OnErrorFn] = None |
371 ) -> Iterator[Tuple[bytes, List[bytes], List[bytes]]]: |
394 ) -> Iterator[Tuple[bytes, List[bytes], List[bytes]]]: |
372 """Yield (dirpath, dirs, files) tuple for each directory under path |
395 """Yield (dirpath, dirs, files) tuple for each directory under path |
373 |
396 |
374 ``dirpath`` is relative one from the root of this vfs. This |
397 ``dirpath`` is relative one from the root of this vfs. This |
375 uses ``os.sep`` as path separator, even you specify POSIX |
398 uses ``os.sep`` as path separator, even you specify POSIX |
384 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror): |
407 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror): |
385 yield (dirpath[prefixlen:], dirs, files) |
408 yield (dirpath[prefixlen:], dirs, files) |
386 |
409 |
387 @contextlib.contextmanager |
410 @contextlib.contextmanager |
388 def backgroundclosing( |
411 def backgroundclosing( |
389 self, ui, expectedcount=-1 |
412 self, ui: uimod.ui, expectedcount: int = -1 |
390 ) -> Iterator[Optional[backgroundfilecloser]]: |
413 ) -> Iterator[Optional[backgroundfilecloser]]: |
391 """Allow files to be closed asynchronously. |
414 """Allow files to be closed asynchronously. |
392 |
415 |
393 When this context manager is active, ``backgroundclose`` can be passed |
416 When this context manager is active, ``backgroundclose`` can be passed |
394 to ``__call__``/``open`` to result in the file possibly being closed |
417 to ``__call__``/``open`` to result in the file possibly being closed |
415 finally: |
438 finally: |
416 vfs._backgroundfilecloser = ( |
439 vfs._backgroundfilecloser = ( |
417 None # pytype: disable=attribute-error |
440 None # pytype: disable=attribute-error |
418 ) |
441 ) |
419 |
442 |
420 def register_file(self, path) -> None: |
443 def register_file(self, path: bytes) -> None: |
421 """generic hook point to lets fncache steer its stew""" |
444 """generic hook point to lets fncache steer its stew""" |
422 |
445 |
423 |
446 |
424 class vfs(abstractvfs): |
447 class vfs(abstractvfs): |
425 """Operate files relative to a base directory |
448 """Operate files relative to a base directory |
430 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or |
453 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or |
431 (b) the base directory is managed by hg and considered sort-of append-only. |
454 (b) the base directory is managed by hg and considered sort-of append-only. |
432 See pathutil.pathauditor() for details. |
455 See pathutil.pathauditor() for details. |
433 """ |
456 """ |
434 |
457 |
|
458 createmode: Optional[int] |
|
459 |
435 def __init__( |
460 def __init__( |
436 self, |
461 self, |
437 base: bytes, |
462 base: bytes, |
438 audit=True, |
463 audit: bool = True, |
439 cacheaudited=False, |
464 cacheaudited: bool = False, |
440 expandpath=False, |
465 expandpath: bool = False, |
441 realpath=False, |
466 realpath: bool = False, |
442 ) -> None: |
467 ) -> None: |
443 if expandpath: |
468 if expandpath: |
444 base = util.expandpath(base) |
469 base = util.expandpath(base) |
445 if realpath: |
470 if realpath: |
446 base = os.path.realpath(base) |
471 base = os.path.realpath(base) |
457 @util.propertycache |
482 @util.propertycache |
458 def _cansymlink(self) -> bool: |
483 def _cansymlink(self) -> bool: |
459 return util.checklink(self.base) |
484 return util.checklink(self.base) |
460 |
485 |
461 @util.propertycache |
486 @util.propertycache |
462 def _chmod(self): |
487 def _chmod(self) -> bool: |
463 return util.checkexec(self.base) |
488 return util.checkexec(self.base) |
464 |
489 |
465 def _fixfilemode(self, name) -> None: |
490 def _fixfilemode(self, name: bytes) -> None: |
466 if self.createmode is None or not self._chmod: |
491 if self.createmode is None or not self._chmod: |
467 return |
492 return |
468 os.chmod(name, self.createmode & 0o666) |
493 os.chmod(name, self.createmode & 0o666) |
469 |
494 |
470 def _auditpath(self, path, mode) -> None: |
495 def _auditpath(self, path: bytes, mode: bytes) -> None: |
471 if self._audit: |
496 if self._audit: |
472 if os.path.isabs(path) and path.startswith(self.base): |
497 if os.path.isabs(path) and path.startswith(self.base): |
473 path = os.path.relpath(path, self.base) |
498 path = os.path.relpath(path, self.base) |
474 r = util.checkosfilename(path) |
499 r = util.checkosfilename(path) |
475 if r: |
500 if r: |
476 raise error.Abort(b"%s: %r" % (r, path)) |
501 raise error.Abort(b"%s: %r" % (r, path)) |
477 self.audit(path, mode=mode) |
502 self.audit(path, mode=mode) |
478 |
503 |
479 def isfileorlink_checkdir( |
504 def isfileorlink_checkdir( |
480 self, dircache, path: Optional[bytes] = None |
505 self, |
|
506 dircache: MutableMapping[bytes, bool], |
|
507 path: Optional[bytes] = None, |
481 ) -> bool: |
508 ) -> bool: |
482 """return True if the path is a regular file or a symlink and |
509 """return True if the path is a regular file or a symlink and |
483 the directories along the path are "normal", that is |
510 the directories along the path are "normal", that is |
484 not symlinks or nested hg repositories. |
511 not symlinks or nested hg repositories. |
485 |
512 |
486 Ignores the `_audit` setting, and checks the directories regardless. |
513 Ignores the `_audit` setting, and checks the directories regardless. |
487 `dircache` is used to cache the directory checks. |
514 `dircache` is used to cache the directory checks. |
488 """ |
515 """ |
|
516 # TODO: Should be a None check on 'path', or shouldn't default to None |
|
517 # because of the immediate call to util.localpath(). |
489 try: |
518 try: |
490 for prefix in pathutil.finddirs_rev_noroot(util.localpath(path)): |
519 for prefix in pathutil.finddirs_rev_noroot(util.localpath(path)): |
491 if prefix in dircache: |
520 if prefix in dircache: |
492 res = dircache[prefix] |
521 res = dircache[prefix] |
493 else: |
522 else: |
503 |
532 |
504 def __call__( |
533 def __call__( |
505 self, |
534 self, |
506 path: bytes, |
535 path: bytes, |
507 mode: bytes = b"rb", |
536 mode: bytes = b"rb", |
508 atomictemp=False, |
537 atomictemp: bool = False, |
509 notindexed=False, |
538 notindexed: bool = False, |
510 backgroundclose=False, |
539 backgroundclose: bool = False, |
511 checkambig=False, |
540 checkambig: bool = False, |
512 auditpath=True, |
541 auditpath: bool = True, |
513 makeparentdirs=True, |
542 makeparentdirs: bool = True, |
514 ) -> Any: |
543 ) -> Any: # TODO: should be BinaryIO if util.atomictempfile can be coersed |
515 """Open ``path`` file, which is relative to vfs root. |
544 """Open ``path`` file, which is relative to vfs root. |
516 |
545 |
517 By default, parent directories are created as needed. Newly created |
546 By default, parent directories are created as needed. Newly created |
518 directories are marked as "not to be indexed by the content indexing |
547 directories are marked as "not to be indexed by the content indexing |
519 service", if ``notindexed`` is specified for "write" mode access. |
548 service", if ``notindexed`` is specified for "write" mode access. |
648 |
677 |
649 opener: Type[vfs] = vfs |
678 opener: Type[vfs] = vfs |
650 |
679 |
651 |
680 |
652 class proxyvfs(abstractvfs, abc.ABC): |
681 class proxyvfs(abstractvfs, abc.ABC): |
653 def __init__(self, vfs: "vfs"): |
682 def __init__(self, vfs: vfs) -> None: |
654 self.vfs = vfs |
683 self.vfs = vfs |
655 |
684 |
656 @property |
685 @property |
657 def createmode(self): |
686 def createmode(self) -> Optional[int]: |
658 return self.vfs.createmode |
687 return self.vfs.createmode |
659 |
688 |
660 def _auditpath(self, path, mode) -> None: |
689 def _auditpath(self, path: bytes, mode: bytes) -> None: |
661 return self.vfs._auditpath(path, mode) |
690 return self.vfs._auditpath(path, mode) |
662 |
691 |
663 @property |
692 @property |
664 def options(self): |
693 def options(self): |
665 return self.vfs.options |
694 return self.vfs.options |
674 |
703 |
675 |
704 |
676 class filtervfs(proxyvfs, abstractvfs): |
705 class filtervfs(proxyvfs, abstractvfs): |
677 '''Wrapper vfs for filtering filenames with a function.''' |
706 '''Wrapper vfs for filtering filenames with a function.''' |
678 |
707 |
679 def __init__(self, vfs: "vfs", filter): |
708 def __init__(self, vfs: vfs, filter) -> None: |
680 proxyvfs.__init__(self, vfs) |
709 proxyvfs.__init__(self, vfs) |
681 self._filter = filter |
710 self._filter = filter |
682 |
711 |
|
712 # TODO: The return type should be BinaryIO |
683 def __call__(self, path: bytes, *args, **kwargs) -> Any: |
713 def __call__(self, path: bytes, *args, **kwargs) -> Any: |
684 return self.vfs(self._filter(path), *args, **kwargs) |
714 return self.vfs(self._filter(path), *args, **kwargs) |
685 |
715 |
686 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes: |
716 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes: |
687 if path: |
717 if path: |
694 |
724 |
695 |
725 |
696 class readonlyvfs(proxyvfs): |
726 class readonlyvfs(proxyvfs): |
697 '''Wrapper vfs preventing any writing.''' |
727 '''Wrapper vfs preventing any writing.''' |
698 |
728 |
699 def __init__(self, vfs: "vfs"): |
729 def __init__(self, vfs: vfs) -> None: |
700 proxyvfs.__init__(self, vfs) |
730 proxyvfs.__init__(self, vfs) |
701 |
731 |
|
732 # TODO: The return type should be BinaryIO |
702 def __call__(self, path: bytes, mode: bytes = b'rb', *args, **kw) -> Any: |
733 def __call__(self, path: bytes, mode: bytes = b'rb', *args, **kw) -> Any: |
703 if mode not in (b'r', b'rb'): |
734 if mode not in (b'r', b'rb'): |
704 raise error.Abort(_(b'this vfs is read only')) |
735 raise error.Abort(_(b'this vfs is read only')) |
705 return self.vfs(path, mode, *args, **kw) |
736 return self.vfs(path, mode, *args, **kw) |
706 |
737 |
715 """ |
746 """ |
716 |
747 |
717 def __init__(self, fh) -> None: |
748 def __init__(self, fh) -> None: |
718 object.__setattr__(self, '_origfh', fh) |
749 object.__setattr__(self, '_origfh', fh) |
719 |
750 |
720 def __getattr__(self, attr) -> Any: |
751 def __getattr__(self, attr: str) -> Any: |
721 return getattr(self._origfh, attr) |
752 return getattr(self._origfh, attr) |
722 |
753 |
723 def __setattr__(self, attr, value) -> None: |
754 def __setattr__(self, attr: str, value: Any) -> None: |
724 return setattr(self._origfh, attr, value) |
755 return setattr(self._origfh, attr, value) |
725 |
756 |
726 def __delattr__(self, attr) -> None: |
757 def __delattr__(self, attr: str) -> None: |
727 return delattr(self._origfh, attr) |
758 return delattr(self._origfh, attr) |
728 |
759 |
729 def __enter__(self: _Tclosewrapbase) -> _Tclosewrapbase: |
760 def __enter__(self: _Tclosewrapbase) -> _Tclosewrapbase: |
730 self._origfh.__enter__() |
761 self._origfh.__enter__() |
731 return self |
762 return self |
757 |
788 |
758 |
789 |
759 class backgroundfilecloser: |
790 class backgroundfilecloser: |
760 """Coordinates background closing of file handles on multiple threads.""" |
791 """Coordinates background closing of file handles on multiple threads.""" |
761 |
792 |
762 def __init__(self, ui, expectedcount=-1) -> None: |
793 def __init__(self, ui: uimod.ui, expectedcount: int = -1) -> None: |
763 self._running = False |
794 self._running = False |
764 self._entered = False |
795 self._entered = False |
765 self._threads = [] |
796 self._threads = [] |
766 self._threadexception = None |
797 self._threadexception = None |
767 |
798 |