Mercurial > public > mercurial-scm > hg
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 |