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