##// END OF EJS Templates
typing: add basic type hints to vfs.py...
Matt Harbison -
r50468:cc9a6005 default
parent child Browse files
Show More
@@ -1,768 +1,788 b''
1 1 # vfs.py - Mercurial 'vfs' classes
2 2 #
3 3 # Copyright Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import contextlib
9 9 import os
10 10 import shutil
11 11 import stat
12 12 import threading
13 13
14 from typing import (
15 Optional,
16 )
17
14 18 from .i18n import _
15 19 from .pycompat import (
16 20 delattr,
17 21 getattr,
18 22 setattr,
19 23 )
20 24 from . import (
21 25 encoding,
22 26 error,
23 27 pathutil,
24 28 pycompat,
25 29 util,
26 30 )
27 31
28 32
29 def _avoidambig(path, oldstat):
33 def _avoidambig(path: bytes, oldstat):
30 34 """Avoid file stat ambiguity forcibly
31 35
32 36 This function causes copying ``path`` file, if it is owned by
33 37 another (see issue5418 and issue5584 for detail).
34 38 """
35 39
36 40 def checkandavoid():
37 41 newstat = util.filestat.frompath(path)
38 42 # return whether file stat ambiguity is (already) avoided
39 43 return not newstat.isambig(oldstat) or newstat.avoidambig(path, oldstat)
40 44
41 45 if not checkandavoid():
42 46 # simply copy to change owner of path to get privilege to
43 47 # advance mtime (see issue5418)
44 48 util.rename(util.mktempcopy(path), path)
45 49 checkandavoid()
46 50
47 51
48 52 class abstractvfs:
49 53 """Abstract base class; cannot be instantiated"""
50 54
51 55 # default directory separator for vfs
52 56 #
53 57 # Other vfs code always use `/` and this works fine because python file API
54 58 # abstract the use of `/` and make it work transparently. For consistency
55 59 # vfs will always use `/` when joining. This avoid some confusion in
56 60 # encoded vfs (see issue6546)
57 61 _dir_sep = b'/'
58 62
59 63 def __init__(self, *args, **kwargs):
60 64 '''Prevent instantiation; don't call this from subclasses.'''
61 65 raise NotImplementedError('attempted instantiating ' + str(type(self)))
62 66
63 def __call__(self, path, mode=b'rb', **kwargs):
67 # TODO: type return, which is util.posixfile wrapped by a proxy
68 def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs):
64 69 raise NotImplementedError
65 70
66 def _auditpath(self, path, mode):
71 def _auditpath(self, path: bytes, mode: bytes):
67 72 raise NotImplementedError
68 73
69 def join(self, path, *insidef):
74 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
70 75 raise NotImplementedError
71 76
72 def tryread(self, path):
77 def tryread(self, path: bytes) -> bytes:
73 78 '''gracefully return an empty string for missing files'''
74 79 try:
75 80 return self.read(path)
76 81 except FileNotFoundError:
77 82 pass
78 83 return b""
79 84
80 def tryreadlines(self, path, mode=b'rb'):
85 def tryreadlines(self, path: bytes, mode: bytes = b'rb'):
81 86 '''gracefully return an empty array for missing files'''
82 87 try:
83 88 return self.readlines(path, mode=mode)
84 89 except FileNotFoundError:
85 90 pass
86 91 return []
87 92
88 93 @util.propertycache
89 94 def open(self):
90 95 """Open ``path`` file, which is relative to vfs root.
91 96
92 97 Newly created directories are marked as "not to be indexed by
93 98 the content indexing service", if ``notindexed`` is specified
94 99 for "write" mode access.
95 100 """
96 101 return self.__call__
97 102
98 def read(self, path):
103 def read(self, path: bytes) -> bytes:
99 104 with self(path, b'rb') as fp:
100 105 return fp.read()
101 106
102 def readlines(self, path, mode=b'rb'):
107 def readlines(self, path: bytes, mode: bytes = b'rb'):
103 108 with self(path, mode=mode) as fp:
104 109 return fp.readlines()
105 110
106 def write(self, path, data, backgroundclose=False, **kwargs):
111 def write(
112 self, path: bytes, data: bytes, backgroundclose=False, **kwargs
113 ) -> int:
107 114 with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp:
108 115 return fp.write(data)
109 116
110 def writelines(self, path, data, mode=b'wb', notindexed=False):
117 def writelines(
118 self, path: bytes, data: bytes, mode: bytes = b'wb', notindexed=False
119 ) -> None:
111 120 with self(path, mode=mode, notindexed=notindexed) as fp:
112 121 return fp.writelines(data)
113 122
114 def append(self, path, data):
123 def append(self, path: bytes, data: bytes) -> int:
115 124 with self(path, b'ab') as fp:
116 125 return fp.write(data)
117 126
118 def basename(self, path):
127 def basename(self, path: bytes) -> bytes:
119 128 """return base element of a path (as os.path.basename would do)
120 129
121 130 This exists to allow handling of strange encoding if needed."""
122 131 return os.path.basename(path)
123 132
124 def chmod(self, path, mode):
133 def chmod(self, path: bytes, mode: int) -> None:
125 134 return os.chmod(self.join(path), mode)
126 135
127 def dirname(self, path):
136 def dirname(self, path: bytes) -> bytes:
128 137 """return dirname element of a path (as os.path.dirname would do)
129 138
130 139 This exists to allow handling of strange encoding if needed."""
131 140 return os.path.dirname(path)
132 141
133 def exists(self, path=None):
142 def exists(self, path: Optional[bytes] = None) -> bool:
134 143 return os.path.exists(self.join(path))
135 144
136 145 def fstat(self, fp):
137 146 return util.fstat(fp)
138 147
139 def isdir(self, path=None):
148 def isdir(self, path: Optional[bytes] = None) -> bool:
140 149 return os.path.isdir(self.join(path))
141 150
142 def isfile(self, path=None):
151 def isfile(self, path: Optional[bytes] = None) -> bool:
143 152 return os.path.isfile(self.join(path))
144 153
145 def islink(self, path=None):
154 def islink(self, path: Optional[bytes] = None) -> bool:
146 155 return os.path.islink(self.join(path))
147 156
148 def isfileorlink(self, path=None):
157 def isfileorlink(self, path: Optional[bytes] = None) -> bool:
149 158 """return whether path is a regular file or a symlink
150 159
151 160 Unlike isfile, this doesn't follow symlinks."""
152 161 try:
153 162 st = self.lstat(path)
154 163 except OSError:
155 164 return False
156 165 mode = st.st_mode
157 166 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
158 167
159 def _join(self, *paths):
168 def _join(self, *paths: bytes) -> bytes:
160 169 root_idx = 0
161 170 for idx, p in enumerate(paths):
162 171 if os.path.isabs(p) or p.startswith(self._dir_sep):
163 172 root_idx = idx
164 173 if root_idx != 0:
165 174 paths = paths[root_idx:]
166 175 paths = [p for p in paths if p]
167 176 return self._dir_sep.join(paths)
168 177
169 def reljoin(self, *paths):
178 def reljoin(self, *paths: bytes) -> bytes:
170 179 """join various elements of a path together (as os.path.join would do)
171 180
172 181 The vfs base is not injected so that path stay relative. This exists
173 182 to allow handling of strange encoding if needed."""
174 183 return self._join(*paths)
175 184
176 def split(self, path):
185 def split(self, path: bytes):
177 186 """split top-most element of a path (as os.path.split would do)
178 187
179 188 This exists to allow handling of strange encoding if needed."""
180 189 return os.path.split(path)
181 190
182 def lexists(self, path=None):
191 def lexists(self, path: Optional[bytes] = None) -> bool:
183 192 return os.path.lexists(self.join(path))
184 193
185 def lstat(self, path=None):
194 def lstat(self, path: Optional[bytes] = None):
186 195 return os.lstat(self.join(path))
187 196
188 def listdir(self, path=None):
197 def listdir(self, path: Optional[bytes] = None):
189 198 return os.listdir(self.join(path))
190 199
191 def makedir(self, path=None, notindexed=True):
200 def makedir(self, path: Optional[bytes] = None, notindexed=True):
192 201 return util.makedir(self.join(path), notindexed)
193 202
194 def makedirs(self, path=None, mode=None):
203 def makedirs(
204 self, path: Optional[bytes] = None, mode: Optional[int] = None
205 ):
195 206 return util.makedirs(self.join(path), mode)
196 207
197 def makelock(self, info, path):
208 def makelock(self, info, path: bytes):
198 209 return util.makelock(info, self.join(path))
199 210
200 def mkdir(self, path=None):
211 def mkdir(self, path: Optional[bytes] = None):
201 212 return os.mkdir(self.join(path))
202 213
203 def mkstemp(self, suffix=b'', prefix=b'tmp', dir=None):
214 def mkstemp(
215 self,
216 suffix: bytes = b'',
217 prefix: bytes = b'tmp',
218 dir: Optional[bytes] = None,
219 ):
204 220 fd, name = pycompat.mkstemp(
205 221 suffix=suffix, prefix=prefix, dir=self.join(dir)
206 222 )
207 223 dname, fname = util.split(name)
208 224 if dir:
209 225 return fd, os.path.join(dir, fname)
210 226 else:
211 227 return fd, fname
212 228
213 def readdir(self, path=None, stat=None, skip=None):
229 def readdir(self, path: Optional[bytes] = None, stat=None, skip=None):
214 230 return util.listdir(self.join(path), stat, skip)
215 231
216 def readlock(self, path):
232 def readlock(self, path: bytes) -> bytes:
217 233 return util.readlock(self.join(path))
218 234
219 def rename(self, src, dst, checkambig=False):
235 def rename(self, src: bytes, dst: bytes, checkambig=False):
220 236 """Rename from src to dst
221 237
222 238 checkambig argument is used with util.filestat, and is useful
223 239 only if destination file is guarded by any lock
224 240 (e.g. repo.lock or repo.wlock).
225 241
226 242 To avoid file stat ambiguity forcibly, checkambig=True involves
227 243 copying ``src`` file, if it is owned by another. Therefore, use
228 244 checkambig=True only in limited cases (see also issue5418 and
229 245 issue5584 for detail).
230 246 """
231 247 self._auditpath(dst, b'w')
232 248 srcpath = self.join(src)
233 249 dstpath = self.join(dst)
234 250 oldstat = checkambig and util.filestat.frompath(dstpath)
235 251 if oldstat and oldstat.stat:
236 252 ret = util.rename(srcpath, dstpath)
237 253 _avoidambig(dstpath, oldstat)
238 254 return ret
239 255 return util.rename(srcpath, dstpath)
240 256
241 def readlink(self, path):
257 def readlink(self, path: bytes) -> bytes:
242 258 return util.readlink(self.join(path))
243 259
244 def removedirs(self, path=None):
260 def removedirs(self, path: Optional[bytes] = None):
245 261 """Remove a leaf directory and all empty intermediate ones"""
246 262 return util.removedirs(self.join(path))
247 263
248 def rmdir(self, path=None):
264 def rmdir(self, path: Optional[bytes] = None):
249 265 """Remove an empty directory."""
250 266 return os.rmdir(self.join(path))
251 267
252 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
268 def rmtree(
269 self, path: Optional[bytes] = None, ignore_errors=False, forcibly=False
270 ):
253 271 """Remove a directory tree recursively
254 272
255 273 If ``forcibly``, this tries to remove READ-ONLY files, too.
256 274 """
257 275 if forcibly:
258 276
259 277 def onerror(function, path, excinfo):
260 278 if function is not os.remove:
261 279 raise
262 280 # read-only files cannot be unlinked under Windows
263 281 s = os.stat(path)
264 282 if (s.st_mode & stat.S_IWRITE) != 0:
265 283 raise
266 284 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
267 285 os.remove(path)
268 286
269 287 else:
270 288 onerror = None
271 289 return shutil.rmtree(
272 290 self.join(path), ignore_errors=ignore_errors, onerror=onerror
273 291 )
274 292
275 def setflags(self, path, l, x):
293 def setflags(self, path: bytes, l: bool, x: bool):
276 294 return util.setflags(self.join(path), l, x)
277 295
278 def stat(self, path=None):
296 def stat(self, path: Optional[bytes] = None):
279 297 return os.stat(self.join(path))
280 298
281 def unlink(self, path=None):
299 def unlink(self, path: Optional[bytes] = None):
282 300 return util.unlink(self.join(path))
283 301
284 def tryunlink(self, path=None):
302 def tryunlink(self, path: Optional[bytes] = None):
285 303 """Attempt to remove a file, ignoring missing file errors."""
286 304 util.tryunlink(self.join(path))
287 305
288 def unlinkpath(self, path=None, ignoremissing=False, rmdir=True):
306 def unlinkpath(
307 self, path: Optional[bytes] = None, ignoremissing=False, rmdir=True
308 ):
289 309 return util.unlinkpath(
290 310 self.join(path), ignoremissing=ignoremissing, rmdir=rmdir
291 311 )
292 312
293 def utime(self, path=None, t=None):
313 def utime(self, path: Optional[bytes] = None, t=None):
294 314 return os.utime(self.join(path), t)
295 315
296 def walk(self, path=None, onerror=None):
316 def walk(self, path: Optional[bytes] = None, onerror=None):
297 317 """Yield (dirpath, dirs, files) tuple for each directories under path
298 318
299 319 ``dirpath`` is relative one from the root of this vfs. This
300 320 uses ``os.sep`` as path separator, even you specify POSIX
301 321 style ``path``.
302 322
303 323 "The root of this vfs" is represented as empty ``dirpath``.
304 324 """
305 325 root = os.path.normpath(self.join(None))
306 326 # when dirpath == root, dirpath[prefixlen:] becomes empty
307 327 # because len(dirpath) < prefixlen.
308 328 prefixlen = len(pathutil.normasprefix(root))
309 329 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
310 330 yield (dirpath[prefixlen:], dirs, files)
311 331
312 332 @contextlib.contextmanager
313 333 def backgroundclosing(self, ui, expectedcount=-1):
314 334 """Allow files to be closed asynchronously.
315 335
316 336 When this context manager is active, ``backgroundclose`` can be passed
317 337 to ``__call__``/``open`` to result in the file possibly being closed
318 338 asynchronously, on a background thread.
319 339 """
320 340 # Sharing backgroundfilecloser between threads is complex and using
321 341 # multiple instances puts us at risk of running out of file descriptors
322 342 # only allow to use backgroundfilecloser when in main thread.
323 343 if not isinstance(
324 344 threading.current_thread(),
325 345 threading._MainThread, # pytype: disable=module-attr
326 346 ):
327 347 yield
328 348 return
329 349 vfs = getattr(self, 'vfs', self)
330 350 if getattr(vfs, '_backgroundfilecloser', None):
331 351 raise error.Abort(
332 352 _(b'can only have 1 active background file closer')
333 353 )
334 354
335 355 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
336 356 try:
337 357 vfs._backgroundfilecloser = (
338 358 bfc # pytype: disable=attribute-error
339 359 )
340 360 yield bfc
341 361 finally:
342 362 vfs._backgroundfilecloser = (
343 363 None # pytype: disable=attribute-error
344 364 )
345 365
346 366 def register_file(self, path):
347 367 """generic hook point to lets fncache steer its stew"""
348 368
349 369
350 370 class vfs(abstractvfs):
351 371 """Operate files relative to a base directory
352 372
353 373 This class is used to hide the details of COW semantics and
354 374 remote file access from higher level code.
355 375
356 376 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
357 377 (b) the base directory is managed by hg and considered sort-of append-only.
358 378 See pathutil.pathauditor() for details.
359 379 """
360 380
361 381 def __init__(
362 382 self,
363 base,
383 base: bytes,
364 384 audit=True,
365 385 cacheaudited=False,
366 386 expandpath=False,
367 387 realpath=False,
368 388 ):
369 389 if expandpath:
370 390 base = util.expandpath(base)
371 391 if realpath:
372 392 base = os.path.realpath(base)
373 393 self.base = base
374 394 self._audit = audit
375 395 if audit:
376 396 self.audit = pathutil.pathauditor(self.base, cached=cacheaudited)
377 397 else:
378 398 self.audit = lambda path, mode=None: True
379 399 self.createmode = None
380 400 self._trustnlink = None
381 401 self.options = {}
382 402
383 403 @util.propertycache
384 def _cansymlink(self):
404 def _cansymlink(self) -> bool:
385 405 return util.checklink(self.base)
386 406
387 407 @util.propertycache
388 408 def _chmod(self):
389 409 return util.checkexec(self.base)
390 410
391 411 def _fixfilemode(self, name):
392 412 if self.createmode is None or not self._chmod:
393 413 return
394 414 os.chmod(name, self.createmode & 0o666)
395 415
396 def _auditpath(self, path, mode):
416 def _auditpath(self, path, mode) -> None:
397 417 if self._audit:
398 418 if os.path.isabs(path) and path.startswith(self.base):
399 419 path = os.path.relpath(path, self.base)
400 420 r = util.checkosfilename(path)
401 421 if r:
402 422 raise error.Abort(b"%s: %r" % (r, path))
403 423 self.audit(path, mode=mode)
404 424
405 425 def __call__(
406 426 self,
407 path,
408 mode=b"r",
427 path: bytes,
428 mode: bytes = b"r",
409 429 atomictemp=False,
410 430 notindexed=False,
411 431 backgroundclose=False,
412 432 checkambig=False,
413 433 auditpath=True,
414 434 makeparentdirs=True,
415 435 ):
416 436 """Open ``path`` file, which is relative to vfs root.
417 437
418 438 By default, parent directories are created as needed. Newly created
419 439 directories are marked as "not to be indexed by the content indexing
420 440 service", if ``notindexed`` is specified for "write" mode access.
421 441 Set ``makeparentdirs=False`` to not create directories implicitly.
422 442
423 443 If ``backgroundclose`` is passed, the file may be closed asynchronously.
424 444 It can only be used if the ``self.backgroundclosing()`` context manager
425 445 is active. This should only be specified if the following criteria hold:
426 446
427 447 1. There is a potential for writing thousands of files. Unless you
428 448 are writing thousands of files, the performance benefits of
429 449 asynchronously closing files is not realized.
430 450 2. Files are opened exactly once for the ``backgroundclosing``
431 451 active duration and are therefore free of race conditions between
432 452 closing a file on a background thread and reopening it. (If the
433 453 file were opened multiple times, there could be unflushed data
434 454 because the original file handle hasn't been flushed/closed yet.)
435 455
436 456 ``checkambig`` argument is passed to atomictempfile (valid
437 457 only for writing), and is useful only if target file is
438 458 guarded by any lock (e.g. repo.lock or repo.wlock).
439 459
440 460 To avoid file stat ambiguity forcibly, checkambig=True involves
441 461 copying ``path`` file opened in "append" mode (e.g. for
442 462 truncation), if it is owned by another. Therefore, use
443 463 combination of append mode and checkambig=True only in limited
444 464 cases (see also issue5418 and issue5584 for detail).
445 465 """
446 466 if auditpath:
447 467 self._auditpath(path, mode)
448 468 f = self.join(path)
449 469
450 470 if b"b" not in mode:
451 471 mode += b"b" # for that other OS
452 472
453 473 nlink = -1
454 474 if mode not in (b'r', b'rb'):
455 475 dirname, basename = util.split(f)
456 476 # If basename is empty, then the path is malformed because it points
457 477 # to a directory. Let the posixfile() call below raise IOError.
458 478 if basename:
459 479 if atomictemp:
460 480 if makeparentdirs:
461 481 util.makedirs(dirname, self.createmode, notindexed)
462 482 return util.atomictempfile(
463 483 f, mode, self.createmode, checkambig=checkambig
464 484 )
465 485 try:
466 486 if b'w' in mode:
467 487 util.unlink(f)
468 488 nlink = 0
469 489 else:
470 490 # nlinks() may behave differently for files on Windows
471 491 # shares if the file is open.
472 492 with util.posixfile(f):
473 493 nlink = util.nlinks(f)
474 494 if nlink < 1:
475 495 nlink = 2 # force mktempcopy (issue1922)
476 496 except FileNotFoundError:
477 497 nlink = 0
478 498 if makeparentdirs:
479 499 util.makedirs(dirname, self.createmode, notindexed)
480 500 if nlink > 0:
481 501 if self._trustnlink is None:
482 502 self._trustnlink = nlink > 1 or util.checknlink(f)
483 503 if nlink > 1 or not self._trustnlink:
484 504 util.rename(util.mktempcopy(f), f)
485 505 fp = util.posixfile(f, mode)
486 506 if nlink == 0:
487 507 self._fixfilemode(f)
488 508
489 509 if checkambig:
490 510 if mode in (b'r', b'rb'):
491 511 raise error.Abort(
492 512 _(
493 513 b'implementation error: mode %s is not'
494 514 b' valid for checkambig=True'
495 515 )
496 516 % mode
497 517 )
498 518 fp = checkambigatclosing(fp)
499 519
500 520 if backgroundclose and isinstance(
501 521 threading.current_thread(),
502 522 threading._MainThread, # pytype: disable=module-attr
503 523 ):
504 524 if (
505 525 not self._backgroundfilecloser # pytype: disable=attribute-error
506 526 ):
507 527 raise error.Abort(
508 528 _(
509 529 b'backgroundclose can only be used when a '
510 530 b'backgroundclosing context manager is active'
511 531 )
512 532 )
513 533
514 534 fp = delayclosedfile(
515 535 fp,
516 536 self._backgroundfilecloser, # pytype: disable=attribute-error
517 537 )
518 538
519 539 return fp
520 540
521 def symlink(self, src, dst):
541 def symlink(self, src: bytes, dst: bytes) -> None:
522 542 self.audit(dst)
523 543 linkname = self.join(dst)
524 544 util.tryunlink(linkname)
525 545
526 546 util.makedirs(os.path.dirname(linkname), self.createmode)
527 547
528 548 if self._cansymlink:
529 549 try:
530 550 os.symlink(src, linkname)
531 551 except OSError as err:
532 552 raise OSError(
533 553 err.errno,
534 554 _(b'could not symlink to %r: %s')
535 555 % (src, encoding.strtolocal(err.strerror)),
536 556 linkname,
537 557 )
538 558 else:
539 559 self.write(dst, src)
540 560
541 def join(self, path, *insidef):
561 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
542 562 if path:
543 563 parts = [self.base, path]
544 564 parts.extend(insidef)
545 565 return self._join(*parts)
546 566 else:
547 567 return self.base
548 568
549 569
550 570 opener = vfs
551 571
552 572
553 573 class proxyvfs(abstractvfs):
554 def __init__(self, vfs):
574 def __init__(self, vfs: "vfs"):
555 575 self.vfs = vfs
556 576
557 577 def _auditpath(self, path, mode):
558 578 return self.vfs._auditpath(path, mode)
559 579
560 580 @property
561 581 def options(self):
562 582 return self.vfs.options
563 583
564 584 @options.setter
565 585 def options(self, value):
566 586 self.vfs.options = value
567 587
568 588
569 589 class filtervfs(proxyvfs, abstractvfs):
570 590 '''Wrapper vfs for filtering filenames with a function.'''
571 591
572 def __init__(self, vfs, filter):
592 def __init__(self, vfs: "vfs", filter):
573 593 proxyvfs.__init__(self, vfs)
574 594 self._filter = filter
575 595
576 def __call__(self, path, *args, **kwargs):
596 def __call__(self, path: bytes, *args, **kwargs):
577 597 return self.vfs(self._filter(path), *args, **kwargs)
578 598
579 def join(self, path, *insidef):
599 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
580 600 if path:
581 601 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
582 602 else:
583 603 return self.vfs.join(path)
584 604
585 605
586 606 filteropener = filtervfs
587 607
588 608
589 609 class readonlyvfs(proxyvfs):
590 610 '''Wrapper vfs preventing any writing.'''
591 611
592 def __init__(self, vfs):
612 def __init__(self, vfs: "vfs"):
593 613 proxyvfs.__init__(self, vfs)
594 614
595 def __call__(self, path, mode=b'r', *args, **kw):
615 def __call__(self, path: bytes, mode: bytes = b'r', *args, **kw):
596 616 if mode not in (b'r', b'rb'):
597 617 raise error.Abort(_(b'this vfs is read only'))
598 618 return self.vfs(path, mode, *args, **kw)
599 619
600 def join(self, path, *insidef):
620 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
601 621 return self.vfs.join(path, *insidef)
602 622
603 623
604 624 class closewrapbase:
605 625 """Base class of wrapper, which hooks closing
606 626
607 627 Do not instantiate outside of the vfs layer.
608 628 """
609 629
610 630 def __init__(self, fh):
611 631 object.__setattr__(self, '_origfh', fh)
612 632
613 633 def __getattr__(self, attr):
614 634 return getattr(self._origfh, attr)
615 635
616 636 def __setattr__(self, attr, value):
617 637 return setattr(self._origfh, attr, value)
618 638
619 639 def __delattr__(self, attr):
620 640 return delattr(self._origfh, attr)
621 641
622 642 def __enter__(self):
623 643 self._origfh.__enter__()
624 644 return self
625 645
626 646 def __exit__(self, exc_type, exc_value, exc_tb):
627 647 raise NotImplementedError('attempted instantiating ' + str(type(self)))
628 648
629 649 def close(self):
630 650 raise NotImplementedError('attempted instantiating ' + str(type(self)))
631 651
632 652
633 653 class delayclosedfile(closewrapbase):
634 654 """Proxy for a file object whose close is delayed.
635 655
636 656 Do not instantiate outside of the vfs layer.
637 657 """
638 658
639 659 def __init__(self, fh, closer):
640 660 super(delayclosedfile, self).__init__(fh)
641 661 object.__setattr__(self, '_closer', closer)
642 662
643 663 def __exit__(self, exc_type, exc_value, exc_tb):
644 664 self._closer.close(self._origfh)
645 665
646 666 def close(self):
647 667 self._closer.close(self._origfh)
648 668
649 669
650 670 class backgroundfilecloser:
651 671 """Coordinates background closing of file handles on multiple threads."""
652 672
653 673 def __init__(self, ui, expectedcount=-1):
654 674 self._running = False
655 675 self._entered = False
656 676 self._threads = []
657 677 self._threadexception = None
658 678
659 679 # Only Windows/NTFS has slow file closing. So only enable by default
660 680 # on that platform. But allow to be enabled elsewhere for testing.
661 681 defaultenabled = pycompat.iswindows
662 682 enabled = ui.configbool(b'worker', b'backgroundclose', defaultenabled)
663 683
664 684 if not enabled:
665 685 return
666 686
667 687 # There is overhead to starting and stopping the background threads.
668 688 # Don't do background processing unless the file count is large enough
669 689 # to justify it.
670 690 minfilecount = ui.configint(b'worker', b'backgroundcloseminfilecount')
671 691 # FUTURE dynamically start background threads after minfilecount closes.
672 692 # (We don't currently have any callers that don't know their file count)
673 693 if expectedcount > 0 and expectedcount < minfilecount:
674 694 return
675 695
676 696 maxqueue = ui.configint(b'worker', b'backgroundclosemaxqueue')
677 697 threadcount = ui.configint(b'worker', b'backgroundclosethreadcount')
678 698
679 699 ui.debug(
680 700 b'starting %d threads for background file closing\n' % threadcount
681 701 )
682 702
683 703 self._queue = pycompat.queue.Queue(maxsize=maxqueue)
684 704 self._running = True
685 705
686 706 for i in range(threadcount):
687 707 t = threading.Thread(target=self._worker, name='backgroundcloser')
688 708 self._threads.append(t)
689 709 t.start()
690 710
691 711 def __enter__(self):
692 712 self._entered = True
693 713 return self
694 714
695 715 def __exit__(self, exc_type, exc_value, exc_tb):
696 716 self._running = False
697 717
698 718 # Wait for threads to finish closing so open files don't linger for
699 719 # longer than lifetime of context manager.
700 720 for t in self._threads:
701 721 t.join()
702 722
703 723 def _worker(self):
704 724 """Main routine for worker thread."""
705 725 while True:
706 726 try:
707 727 fh = self._queue.get(block=True, timeout=0.100)
708 728 # Need to catch or the thread will terminate and
709 729 # we could orphan file descriptors.
710 730 try:
711 731 fh.close()
712 732 except Exception as e:
713 733 # Stash so can re-raise from main thread later.
714 734 self._threadexception = e
715 735 except pycompat.queue.Empty:
716 736 if not self._running:
717 737 break
718 738
719 739 def close(self, fh):
720 740 """Schedule a file for closing."""
721 741 if not self._entered:
722 742 raise error.Abort(
723 743 _(b'can only call close() when context manager active')
724 744 )
725 745
726 746 # If a background thread encountered an exception, raise now so we fail
727 747 # fast. Otherwise we may potentially go on for minutes until the error
728 748 # is acted on.
729 749 if self._threadexception:
730 750 e = self._threadexception
731 751 self._threadexception = None
732 752 raise e
733 753
734 754 # If we're not actively running, close synchronously.
735 755 if not self._running:
736 756 fh.close()
737 757 return
738 758
739 759 self._queue.put(fh, block=True, timeout=None)
740 760
741 761
742 762 class checkambigatclosing(closewrapbase):
743 763 """Proxy for a file object, to avoid ambiguity of file stat
744 764
745 765 See also util.filestat for detail about "ambiguity of file stat".
746 766
747 767 This proxy is useful only if the target file is guarded by any
748 768 lock (e.g. repo.lock or repo.wlock)
749 769
750 770 Do not instantiate outside of the vfs layer.
751 771 """
752 772
753 773 def __init__(self, fh):
754 774 super(checkambigatclosing, self).__init__(fh)
755 775 object.__setattr__(self, '_oldstat', util.filestat.frompath(fh.name))
756 776
757 777 def _checkambig(self):
758 778 oldstat = self._oldstat
759 779 if oldstat.stat:
760 780 _avoidambig(self._origfh.name, oldstat)
761 781
762 782 def __exit__(self, exc_type, exc_value, exc_tb):
763 783 self._origfh.__exit__(exc_type, exc_value, exc_tb)
764 784 self._checkambig()
765 785
766 786 def close(self):
767 787 self._origfh.close()
768 788 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now