##// END OF EJS Templates
vfs: ensure closewrapbase fh doesn't escape by entering context manager...
Matt Harbison -
r40975:8d9f366b stable
parent child Browse files
Show More
@@ -1,657 +1,658 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 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 __call__(self, path, mode="r", atomictemp=False, notindexed=False,
341 341 backgroundclose=False, checkambig=False, auditpath=True):
342 342 '''Open ``path`` file, which is relative to vfs root.
343 343
344 344 Newly created directories are marked as "not to be indexed by
345 345 the content indexing service", if ``notindexed`` is specified
346 346 for "write" mode access.
347 347
348 348 If ``backgroundclose`` is passed, the file may be closed asynchronously.
349 349 It can only be used if the ``self.backgroundclosing()`` context manager
350 350 is active. This should only be specified if the following criteria hold:
351 351
352 352 1. There is a potential for writing thousands of files. Unless you
353 353 are writing thousands of files, the performance benefits of
354 354 asynchronously closing files is not realized.
355 355 2. Files are opened exactly once for the ``backgroundclosing``
356 356 active duration and are therefore free of race conditions between
357 357 closing a file on a background thread and reopening it. (If the
358 358 file were opened multiple times, there could be unflushed data
359 359 because the original file handle hasn't been flushed/closed yet.)
360 360
361 361 ``checkambig`` argument is passed to atomictemplfile (valid
362 362 only for writing), and is useful only if target file is
363 363 guarded by any lock (e.g. repo.lock or repo.wlock).
364 364
365 365 To avoid file stat ambiguity forcibly, checkambig=True involves
366 366 copying ``path`` file opened in "append" mode (e.g. for
367 367 truncation), if it is owned by another. Therefore, use
368 368 combination of append mode and checkambig=True only in limited
369 369 cases (see also issue5418 and issue5584 for detail).
370 370 '''
371 371 if auditpath:
372 372 if self._audit:
373 373 r = util.checkosfilename(path)
374 374 if r:
375 375 raise error.Abort("%s: %r" % (r, path))
376 376 self.audit(path, mode=mode)
377 377 f = self.join(path)
378 378
379 379 if "b" not in mode:
380 380 mode += "b" # for that other OS
381 381
382 382 nlink = -1
383 383 if mode not in ('r', 'rb'):
384 384 dirname, basename = util.split(f)
385 385 # If basename is empty, then the path is malformed because it points
386 386 # to a directory. Let the posixfile() call below raise IOError.
387 387 if basename:
388 388 if atomictemp:
389 389 util.makedirs(dirname, self.createmode, notindexed)
390 390 return util.atomictempfile(f, mode, self.createmode,
391 391 checkambig=checkambig)
392 392 try:
393 393 if 'w' in mode:
394 394 util.unlink(f)
395 395 nlink = 0
396 396 else:
397 397 # nlinks() may behave differently for files on Windows
398 398 # shares if the file is open.
399 399 with util.posixfile(f):
400 400 nlink = util.nlinks(f)
401 401 if nlink < 1:
402 402 nlink = 2 # force mktempcopy (issue1922)
403 403 except (OSError, IOError) as e:
404 404 if e.errno != errno.ENOENT:
405 405 raise
406 406 nlink = 0
407 407 util.makedirs(dirname, self.createmode, notindexed)
408 408 if nlink > 0:
409 409 if self._trustnlink is None:
410 410 self._trustnlink = nlink > 1 or util.checknlink(f)
411 411 if nlink > 1 or not self._trustnlink:
412 412 util.rename(util.mktempcopy(f), f)
413 413 fp = util.posixfile(f, mode)
414 414 if nlink == 0:
415 415 self._fixfilemode(f)
416 416
417 417 if checkambig:
418 418 if mode in ('r', 'rb'):
419 419 raise error.Abort(_('implementation error: mode %s is not'
420 420 ' valid for checkambig=True') % mode)
421 421 fp = checkambigatclosing(fp)
422 422
423 423 if (backgroundclose and
424 424 isinstance(threading.currentThread(), threading._MainThread)):
425 425 if not self._backgroundfilecloser:
426 426 raise error.Abort(_('backgroundclose can only be used when a '
427 427 'backgroundclosing context manager is active')
428 428 )
429 429
430 430 fp = delayclosedfile(fp, self._backgroundfilecloser)
431 431
432 432 return fp
433 433
434 434 def symlink(self, src, dst):
435 435 self.audit(dst)
436 436 linkname = self.join(dst)
437 437 util.tryunlink(linkname)
438 438
439 439 util.makedirs(os.path.dirname(linkname), self.createmode)
440 440
441 441 if self._cansymlink:
442 442 try:
443 443 os.symlink(src, linkname)
444 444 except OSError as err:
445 445 raise OSError(err.errno, _('could not symlink to %r: %s') %
446 446 (src, encoding.strtolocal(err.strerror)),
447 447 linkname)
448 448 else:
449 449 self.write(dst, src)
450 450
451 451 def join(self, path, *insidef):
452 452 if path:
453 453 return os.path.join(self.base, path, *insidef)
454 454 else:
455 455 return self.base
456 456
457 457 opener = vfs
458 458
459 459 class proxyvfs(object):
460 460 def __init__(self, vfs):
461 461 self.vfs = vfs
462 462
463 463 @property
464 464 def options(self):
465 465 return self.vfs.options
466 466
467 467 @options.setter
468 468 def options(self, value):
469 469 self.vfs.options = value
470 470
471 471 class filtervfs(abstractvfs, proxyvfs):
472 472 '''Wrapper vfs for filtering filenames with a function.'''
473 473
474 474 def __init__(self, vfs, filter):
475 475 proxyvfs.__init__(self, vfs)
476 476 self._filter = filter
477 477
478 478 def __call__(self, path, *args, **kwargs):
479 479 return self.vfs(self._filter(path), *args, **kwargs)
480 480
481 481 def join(self, path, *insidef):
482 482 if path:
483 483 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
484 484 else:
485 485 return self.vfs.join(path)
486 486
487 487 filteropener = filtervfs
488 488
489 489 class readonlyvfs(abstractvfs, proxyvfs):
490 490 '''Wrapper vfs preventing any writing.'''
491 491
492 492 def __init__(self, vfs):
493 493 proxyvfs.__init__(self, vfs)
494 494
495 495 def __call__(self, path, mode='r', *args, **kw):
496 496 if mode not in ('r', 'rb'):
497 497 raise error.Abort(_('this vfs is read only'))
498 498 return self.vfs(path, mode, *args, **kw)
499 499
500 500 def join(self, path, *insidef):
501 501 return self.vfs.join(path, *insidef)
502 502
503 503 class closewrapbase(object):
504 504 """Base class of wrapper, which hooks closing
505 505
506 506 Do not instantiate outside of the vfs layer.
507 507 """
508 508 def __init__(self, fh):
509 509 object.__setattr__(self, r'_origfh', fh)
510 510
511 511 def __getattr__(self, attr):
512 512 return getattr(self._origfh, attr)
513 513
514 514 def __setattr__(self, attr, value):
515 515 return setattr(self._origfh, attr, value)
516 516
517 517 def __delattr__(self, attr):
518 518 return delattr(self._origfh, attr)
519 519
520 520 def __enter__(self):
521 return self._origfh.__enter__()
521 self._origfh.__enter__()
522 return self
522 523
523 524 def __exit__(self, exc_type, exc_value, exc_tb):
524 525 raise NotImplementedError('attempted instantiating ' + str(type(self)))
525 526
526 527 def close(self):
527 528 raise NotImplementedError('attempted instantiating ' + str(type(self)))
528 529
529 530 class delayclosedfile(closewrapbase):
530 531 """Proxy for a file object whose close is delayed.
531 532
532 533 Do not instantiate outside of the vfs layer.
533 534 """
534 535 def __init__(self, fh, closer):
535 536 super(delayclosedfile, self).__init__(fh)
536 537 object.__setattr__(self, r'_closer', closer)
537 538
538 539 def __exit__(self, exc_type, exc_value, exc_tb):
539 540 self._closer.close(self._origfh)
540 541
541 542 def close(self):
542 543 self._closer.close(self._origfh)
543 544
544 545 class backgroundfilecloser(object):
545 546 """Coordinates background closing of file handles on multiple threads."""
546 547 def __init__(self, ui, expectedcount=-1):
547 548 self._running = False
548 549 self._entered = False
549 550 self._threads = []
550 551 self._threadexception = None
551 552
552 553 # Only Windows/NTFS has slow file closing. So only enable by default
553 554 # on that platform. But allow to be enabled elsewhere for testing.
554 555 defaultenabled = pycompat.iswindows
555 556 enabled = ui.configbool('worker', 'backgroundclose', defaultenabled)
556 557
557 558 if not enabled:
558 559 return
559 560
560 561 # There is overhead to starting and stopping the background threads.
561 562 # Don't do background processing unless the file count is large enough
562 563 # to justify it.
563 564 minfilecount = ui.configint('worker', 'backgroundcloseminfilecount')
564 565 # FUTURE dynamically start background threads after minfilecount closes.
565 566 # (We don't currently have any callers that don't know their file count)
566 567 if expectedcount > 0 and expectedcount < minfilecount:
567 568 return
568 569
569 570 maxqueue = ui.configint('worker', 'backgroundclosemaxqueue')
570 571 threadcount = ui.configint('worker', 'backgroundclosethreadcount')
571 572
572 573 ui.debug('starting %d threads for background file closing\n' %
573 574 threadcount)
574 575
575 576 self._queue = pycompat.queue.Queue(maxsize=maxqueue)
576 577 self._running = True
577 578
578 579 for i in range(threadcount):
579 580 t = threading.Thread(target=self._worker, name='backgroundcloser')
580 581 self._threads.append(t)
581 582 t.start()
582 583
583 584 def __enter__(self):
584 585 self._entered = True
585 586 return self
586 587
587 588 def __exit__(self, exc_type, exc_value, exc_tb):
588 589 self._running = False
589 590
590 591 # Wait for threads to finish closing so open files don't linger for
591 592 # longer than lifetime of context manager.
592 593 for t in self._threads:
593 594 t.join()
594 595
595 596 def _worker(self):
596 597 """Main routine for worker thread."""
597 598 while True:
598 599 try:
599 600 fh = self._queue.get(block=True, timeout=0.100)
600 601 # Need to catch or the thread will terminate and
601 602 # we could orphan file descriptors.
602 603 try:
603 604 fh.close()
604 605 except Exception as e:
605 606 # Stash so can re-raise from main thread later.
606 607 self._threadexception = e
607 608 except pycompat.queue.Empty:
608 609 if not self._running:
609 610 break
610 611
611 612 def close(self, fh):
612 613 """Schedule a file for closing."""
613 614 if not self._entered:
614 615 raise error.Abort(_('can only call close() when context manager '
615 616 'active'))
616 617
617 618 # If a background thread encountered an exception, raise now so we fail
618 619 # fast. Otherwise we may potentially go on for minutes until the error
619 620 # is acted on.
620 621 if self._threadexception:
621 622 e = self._threadexception
622 623 self._threadexception = None
623 624 raise e
624 625
625 626 # If we're not actively running, close synchronously.
626 627 if not self._running:
627 628 fh.close()
628 629 return
629 630
630 631 self._queue.put(fh, block=True, timeout=None)
631 632
632 633 class checkambigatclosing(closewrapbase):
633 634 """Proxy for a file object, to avoid ambiguity of file stat
634 635
635 636 See also util.filestat for detail about "ambiguity of file stat".
636 637
637 638 This proxy is useful only if the target file is guarded by any
638 639 lock (e.g. repo.lock or repo.wlock)
639 640
640 641 Do not instantiate outside of the vfs layer.
641 642 """
642 643 def __init__(self, fh):
643 644 super(checkambigatclosing, self).__init__(fh)
644 645 object.__setattr__(self, r'_oldstat', util.filestat.frompath(fh.name))
645 646
646 647 def _checkambig(self):
647 648 oldstat = self._oldstat
648 649 if oldstat.stat:
649 650 _avoidambig(self._origfh.name, oldstat)
650 651
651 652 def __exit__(self, exc_type, exc_value, exc_tb):
652 653 self._origfh.__exit__(exc_type, exc_value, exc_tb)
653 654 self._checkambig()
654 655
655 656 def close(self):
656 657 self._origfh.close()
657 658 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now