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