##// END OF EJS Templates
vfs: raise NotImplementedError in abstractvfs._auditvfs...
Boris Feld -
r41127:e0d00ec2 default
parent child Browse files
Show More
@@ -1,674 +1,674
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 pass
50 raise NotImplementedError
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 476 def _auditpath(self, path, mode):
477 477 return self.vfs._auditpath(path, mode)
478 478
479 479 @property
480 480 def options(self):
481 481 return self.vfs.options
482 482
483 483 @options.setter
484 484 def options(self, value):
485 485 self.vfs.options = value
486 486
487 487 class filtervfs(proxyvfs, abstractvfs):
488 488 '''Wrapper vfs for filtering filenames with a function.'''
489 489
490 490 def __init__(self, vfs, filter):
491 491 proxyvfs.__init__(self, vfs)
492 492 self._filter = filter
493 493
494 494 def __call__(self, path, *args, **kwargs):
495 495 return self.vfs(self._filter(path), *args, **kwargs)
496 496
497 497 def join(self, path, *insidef):
498 498 if path:
499 499 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
500 500 else:
501 501 return self.vfs.join(path)
502 502
503 503 filteropener = filtervfs
504 504
505 505 class readonlyvfs(proxyvfs):
506 506 '''Wrapper vfs preventing any writing.'''
507 507
508 508 def __init__(self, vfs):
509 509 proxyvfs.__init__(self, vfs)
510 510
511 511 def __call__(self, path, mode='r', *args, **kw):
512 512 if mode not in ('r', 'rb'):
513 513 raise error.Abort(_('this vfs is read only'))
514 514 return self.vfs(path, mode, *args, **kw)
515 515
516 516 def join(self, path, *insidef):
517 517 return self.vfs.join(path, *insidef)
518 518
519 519 class closewrapbase(object):
520 520 """Base class of wrapper, which hooks closing
521 521
522 522 Do not instantiate outside of the vfs layer.
523 523 """
524 524 def __init__(self, fh):
525 525 object.__setattr__(self, r'_origfh', fh)
526 526
527 527 def __getattr__(self, attr):
528 528 return getattr(self._origfh, attr)
529 529
530 530 def __setattr__(self, attr, value):
531 531 return setattr(self._origfh, attr, value)
532 532
533 533 def __delattr__(self, attr):
534 534 return delattr(self._origfh, attr)
535 535
536 536 def __enter__(self):
537 537 self._origfh.__enter__()
538 538 return self
539 539
540 540 def __exit__(self, exc_type, exc_value, exc_tb):
541 541 raise NotImplementedError('attempted instantiating ' + str(type(self)))
542 542
543 543 def close(self):
544 544 raise NotImplementedError('attempted instantiating ' + str(type(self)))
545 545
546 546 class delayclosedfile(closewrapbase):
547 547 """Proxy for a file object whose close is delayed.
548 548
549 549 Do not instantiate outside of the vfs layer.
550 550 """
551 551 def __init__(self, fh, closer):
552 552 super(delayclosedfile, self).__init__(fh)
553 553 object.__setattr__(self, r'_closer', closer)
554 554
555 555 def __exit__(self, exc_type, exc_value, exc_tb):
556 556 self._closer.close(self._origfh)
557 557
558 558 def close(self):
559 559 self._closer.close(self._origfh)
560 560
561 561 class backgroundfilecloser(object):
562 562 """Coordinates background closing of file handles on multiple threads."""
563 563 def __init__(self, ui, expectedcount=-1):
564 564 self._running = False
565 565 self._entered = False
566 566 self._threads = []
567 567 self._threadexception = None
568 568
569 569 # Only Windows/NTFS has slow file closing. So only enable by default
570 570 # on that platform. But allow to be enabled elsewhere for testing.
571 571 defaultenabled = pycompat.iswindows
572 572 enabled = ui.configbool('worker', 'backgroundclose', defaultenabled)
573 573
574 574 if not enabled:
575 575 return
576 576
577 577 # There is overhead to starting and stopping the background threads.
578 578 # Don't do background processing unless the file count is large enough
579 579 # to justify it.
580 580 minfilecount = ui.configint('worker', 'backgroundcloseminfilecount')
581 581 # FUTURE dynamically start background threads after minfilecount closes.
582 582 # (We don't currently have any callers that don't know their file count)
583 583 if expectedcount > 0 and expectedcount < minfilecount:
584 584 return
585 585
586 586 maxqueue = ui.configint('worker', 'backgroundclosemaxqueue')
587 587 threadcount = ui.configint('worker', 'backgroundclosethreadcount')
588 588
589 589 ui.debug('starting %d threads for background file closing\n' %
590 590 threadcount)
591 591
592 592 self._queue = pycompat.queue.Queue(maxsize=maxqueue)
593 593 self._running = True
594 594
595 595 for i in range(threadcount):
596 596 t = threading.Thread(target=self._worker, name='backgroundcloser')
597 597 self._threads.append(t)
598 598 t.start()
599 599
600 600 def __enter__(self):
601 601 self._entered = True
602 602 return self
603 603
604 604 def __exit__(self, exc_type, exc_value, exc_tb):
605 605 self._running = False
606 606
607 607 # Wait for threads to finish closing so open files don't linger for
608 608 # longer than lifetime of context manager.
609 609 for t in self._threads:
610 610 t.join()
611 611
612 612 def _worker(self):
613 613 """Main routine for worker thread."""
614 614 while True:
615 615 try:
616 616 fh = self._queue.get(block=True, timeout=0.100)
617 617 # Need to catch or the thread will terminate and
618 618 # we could orphan file descriptors.
619 619 try:
620 620 fh.close()
621 621 except Exception as e:
622 622 # Stash so can re-raise from main thread later.
623 623 self._threadexception = e
624 624 except pycompat.queue.Empty:
625 625 if not self._running:
626 626 break
627 627
628 628 def close(self, fh):
629 629 """Schedule a file for closing."""
630 630 if not self._entered:
631 631 raise error.Abort(_('can only call close() when context manager '
632 632 'active'))
633 633
634 634 # If a background thread encountered an exception, raise now so we fail
635 635 # fast. Otherwise we may potentially go on for minutes until the error
636 636 # is acted on.
637 637 if self._threadexception:
638 638 e = self._threadexception
639 639 self._threadexception = None
640 640 raise e
641 641
642 642 # If we're not actively running, close synchronously.
643 643 if not self._running:
644 644 fh.close()
645 645 return
646 646
647 647 self._queue.put(fh, block=True, timeout=None)
648 648
649 649 class checkambigatclosing(closewrapbase):
650 650 """Proxy for a file object, to avoid ambiguity of file stat
651 651
652 652 See also util.filestat for detail about "ambiguity of file stat".
653 653
654 654 This proxy is useful only if the target file is guarded by any
655 655 lock (e.g. repo.lock or repo.wlock)
656 656
657 657 Do not instantiate outside of the vfs layer.
658 658 """
659 659 def __init__(self, fh):
660 660 super(checkambigatclosing, self).__init__(fh)
661 661 object.__setattr__(self, r'_oldstat', util.filestat.frompath(fh.name))
662 662
663 663 def _checkambig(self):
664 664 oldstat = self._oldstat
665 665 if oldstat.stat:
666 666 _avoidambig(self._origfh.name, oldstat)
667 667
668 668 def __exit__(self, exc_type, exc_value, exc_tb):
669 669 self._origfh.__exit__(exc_type, exc_value, exc_tb)
670 670 self._checkambig()
671 671
672 672 def close(self):
673 673 self._origfh.close()
674 674 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now