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