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