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