comparison mercurial/vfs.py @ 51888:fa9e8a6521c1

typing: manually add type annotations to `mercurial/vfs.py` This isn't everything, but hopefully it's close enough to hack on a protocol class.
author Matt Harbison <matt_harbison@yahoo.com>
date Fri, 20 Sep 2024 20:16:12 -0400
parents ad83e4f9b40e
children 22e1924e9402
comparison
equal deleted inserted replaced
51887:ad83e4f9b40e 51888:fa9e8a6521c1
15 import threading 15 import threading
16 import typing 16 import typing
17 17
18 from typing import ( 18 from typing import (
19 Any, 19 Any,
20 BinaryIO,
21 Callable,
20 Iterable, 22 Iterable,
21 Iterator, 23 Iterator,
22 List, 24 List,
25 MutableMapping,
23 Optional, 26 Optional,
24 Tuple, 27 Tuple,
25 Type, 28 Type,
26 TypeVar, 29 TypeVar,
27 ) 30 )
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