##// END OF EJS Templates
util: add a timed function for use during development...
Bryan O'Sullivan -
r18736:af9ddea2 default
parent child Browse files
Show More
@@ -1,1874 +1,1917 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from i18n import _
17 17 import error, osutil, encoding, collections
18 18 import errno, re, shutil, sys, tempfile, traceback
19 19 import os, time, datetime, calendar, textwrap, signal
20 20 import imp, socket, urllib
21 21
22 22 if os.name == 'nt':
23 23 import windows as platform
24 24 else:
25 25 import posix as platform
26 26
27 27 cachestat = platform.cachestat
28 28 checkexec = platform.checkexec
29 29 checklink = platform.checklink
30 30 copymode = platform.copymode
31 31 executablepath = platform.executablepath
32 32 expandglobs = platform.expandglobs
33 33 explainexit = platform.explainexit
34 34 findexe = platform.findexe
35 35 gethgcmd = platform.gethgcmd
36 36 getuser = platform.getuser
37 37 groupmembers = platform.groupmembers
38 38 groupname = platform.groupname
39 39 hidewindow = platform.hidewindow
40 40 isexec = platform.isexec
41 41 isowner = platform.isowner
42 42 localpath = platform.localpath
43 43 lookupreg = platform.lookupreg
44 44 makedir = platform.makedir
45 45 nlinks = platform.nlinks
46 46 normpath = platform.normpath
47 47 normcase = platform.normcase
48 48 openhardlinks = platform.openhardlinks
49 49 oslink = platform.oslink
50 50 parsepatchoutput = platform.parsepatchoutput
51 51 pconvert = platform.pconvert
52 52 popen = platform.popen
53 53 posixfile = platform.posixfile
54 54 quotecommand = platform.quotecommand
55 55 realpath = platform.realpath
56 56 rename = platform.rename
57 57 samedevice = platform.samedevice
58 58 samefile = platform.samefile
59 59 samestat = platform.samestat
60 60 setbinary = platform.setbinary
61 61 setflags = platform.setflags
62 62 setsignalhandler = platform.setsignalhandler
63 63 shellquote = platform.shellquote
64 64 spawndetached = platform.spawndetached
65 65 split = platform.split
66 66 sshargs = platform.sshargs
67 67 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
68 68 termwidth = platform.termwidth
69 69 testpid = platform.testpid
70 70 umask = platform.umask
71 71 unlink = platform.unlink
72 72 unlinkpath = platform.unlinkpath
73 73 username = platform.username
74 74
75 75 # Python compatibility
76 76
77 77 _notset = object()
78 78
79 79 def safehasattr(thing, attr):
80 80 return getattr(thing, attr, _notset) is not _notset
81 81
82 82 def sha1(s=''):
83 83 '''
84 84 Low-overhead wrapper around Python's SHA support
85 85
86 86 >>> f = _fastsha1
87 87 >>> a = sha1()
88 88 >>> a = f()
89 89 >>> a.hexdigest()
90 90 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
91 91 '''
92 92
93 93 return _fastsha1(s)
94 94
95 95 def _fastsha1(s=''):
96 96 # This function will import sha1 from hashlib or sha (whichever is
97 97 # available) and overwrite itself with it on the first call.
98 98 # Subsequent calls will go directly to the imported function.
99 99 if sys.version_info >= (2, 5):
100 100 from hashlib import sha1 as _sha1
101 101 else:
102 102 from sha import sha as _sha1
103 103 global _fastsha1, sha1
104 104 _fastsha1 = sha1 = _sha1
105 105 return _sha1(s)
106 106
107 107 try:
108 108 buffer = buffer
109 109 except NameError:
110 110 if sys.version_info[0] < 3:
111 111 def buffer(sliceable, offset=0):
112 112 return sliceable[offset:]
113 113 else:
114 114 def buffer(sliceable, offset=0):
115 115 return memoryview(sliceable)[offset:]
116 116
117 117 import subprocess
118 118 closefds = os.name == 'posix'
119 119
120 120 def popen2(cmd, env=None, newlines=False):
121 121 # Setting bufsize to -1 lets the system decide the buffer size.
122 122 # The default for bufsize is 0, meaning unbuffered. This leads to
123 123 # poor performance on Mac OS X: http://bugs.python.org/issue4194
124 124 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
125 125 close_fds=closefds,
126 126 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
127 127 universal_newlines=newlines,
128 128 env=env)
129 129 return p.stdin, p.stdout
130 130
131 131 def popen3(cmd, env=None, newlines=False):
132 132 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
133 133 close_fds=closefds,
134 134 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
135 135 stderr=subprocess.PIPE,
136 136 universal_newlines=newlines,
137 137 env=env)
138 138 return p.stdin, p.stdout, p.stderr
139 139
140 140 def version():
141 141 """Return version information if available."""
142 142 try:
143 143 import __version__
144 144 return __version__.version
145 145 except ImportError:
146 146 return 'unknown'
147 147
148 148 # used by parsedate
149 149 defaultdateformats = (
150 150 '%Y-%m-%d %H:%M:%S',
151 151 '%Y-%m-%d %I:%M:%S%p',
152 152 '%Y-%m-%d %H:%M',
153 153 '%Y-%m-%d %I:%M%p',
154 154 '%Y-%m-%d',
155 155 '%m-%d',
156 156 '%m/%d',
157 157 '%m/%d/%y',
158 158 '%m/%d/%Y',
159 159 '%a %b %d %H:%M:%S %Y',
160 160 '%a %b %d %I:%M:%S%p %Y',
161 161 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
162 162 '%b %d %H:%M:%S %Y',
163 163 '%b %d %I:%M:%S%p %Y',
164 164 '%b %d %H:%M:%S',
165 165 '%b %d %I:%M:%S%p',
166 166 '%b %d %H:%M',
167 167 '%b %d %I:%M%p',
168 168 '%b %d %Y',
169 169 '%b %d',
170 170 '%H:%M:%S',
171 171 '%I:%M:%S%p',
172 172 '%H:%M',
173 173 '%I:%M%p',
174 174 )
175 175
176 176 extendeddateformats = defaultdateformats + (
177 177 "%Y",
178 178 "%Y-%m",
179 179 "%b",
180 180 "%b %Y",
181 181 )
182 182
183 183 def cachefunc(func):
184 184 '''cache the result of function calls'''
185 185 # XXX doesn't handle keywords args
186 186 cache = {}
187 187 if func.func_code.co_argcount == 1:
188 188 # we gain a small amount of time because
189 189 # we don't need to pack/unpack the list
190 190 def f(arg):
191 191 if arg not in cache:
192 192 cache[arg] = func(arg)
193 193 return cache[arg]
194 194 else:
195 195 def f(*args):
196 196 if args not in cache:
197 197 cache[args] = func(*args)
198 198 return cache[args]
199 199
200 200 return f
201 201
202 202 try:
203 203 collections.deque.remove
204 204 deque = collections.deque
205 205 except AttributeError:
206 206 # python 2.4 lacks deque.remove
207 207 class deque(collections.deque):
208 208 def remove(self, val):
209 209 for i, v in enumerate(self):
210 210 if v == val:
211 211 del self[i]
212 212 break
213 213
214 214 class lrucachedict(object):
215 215 '''cache most recent gets from or sets to this dictionary'''
216 216 def __init__(self, maxsize):
217 217 self._cache = {}
218 218 self._maxsize = maxsize
219 219 self._order = deque()
220 220
221 221 def __getitem__(self, key):
222 222 value = self._cache[key]
223 223 self._order.remove(key)
224 224 self._order.append(key)
225 225 return value
226 226
227 227 def __setitem__(self, key, value):
228 228 if key not in self._cache:
229 229 if len(self._cache) >= self._maxsize:
230 230 del self._cache[self._order.popleft()]
231 231 else:
232 232 self._order.remove(key)
233 233 self._cache[key] = value
234 234 self._order.append(key)
235 235
236 236 def __contains__(self, key):
237 237 return key in self._cache
238 238
239 239 def lrucachefunc(func):
240 240 '''cache most recent results of function calls'''
241 241 cache = {}
242 242 order = deque()
243 243 if func.func_code.co_argcount == 1:
244 244 def f(arg):
245 245 if arg not in cache:
246 246 if len(cache) > 20:
247 247 del cache[order.popleft()]
248 248 cache[arg] = func(arg)
249 249 else:
250 250 order.remove(arg)
251 251 order.append(arg)
252 252 return cache[arg]
253 253 else:
254 254 def f(*args):
255 255 if args not in cache:
256 256 if len(cache) > 20:
257 257 del cache[order.popleft()]
258 258 cache[args] = func(*args)
259 259 else:
260 260 order.remove(args)
261 261 order.append(args)
262 262 return cache[args]
263 263
264 264 return f
265 265
266 266 class propertycache(object):
267 267 def __init__(self, func):
268 268 self.func = func
269 269 self.name = func.__name__
270 270 def __get__(self, obj, type=None):
271 271 result = self.func(obj)
272 272 self.cachevalue(obj, result)
273 273 return result
274 274
275 275 def cachevalue(self, obj, value):
276 276 setattr(obj, self.name, value)
277 277
278 278 def pipefilter(s, cmd):
279 279 '''filter string S through command CMD, returning its output'''
280 280 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
281 281 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
282 282 pout, perr = p.communicate(s)
283 283 return pout
284 284
285 285 def tempfilter(s, cmd):
286 286 '''filter string S through a pair of temporary files with CMD.
287 287 CMD is used as a template to create the real command to be run,
288 288 with the strings INFILE and OUTFILE replaced by the real names of
289 289 the temporary files generated.'''
290 290 inname, outname = None, None
291 291 try:
292 292 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
293 293 fp = os.fdopen(infd, 'wb')
294 294 fp.write(s)
295 295 fp.close()
296 296 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
297 297 os.close(outfd)
298 298 cmd = cmd.replace('INFILE', inname)
299 299 cmd = cmd.replace('OUTFILE', outname)
300 300 code = os.system(cmd)
301 301 if sys.platform == 'OpenVMS' and code & 1:
302 302 code = 0
303 303 if code:
304 304 raise Abort(_("command '%s' failed: %s") %
305 305 (cmd, explainexit(code)))
306 306 fp = open(outname, 'rb')
307 307 r = fp.read()
308 308 fp.close()
309 309 return r
310 310 finally:
311 311 try:
312 312 if inname:
313 313 os.unlink(inname)
314 314 except OSError:
315 315 pass
316 316 try:
317 317 if outname:
318 318 os.unlink(outname)
319 319 except OSError:
320 320 pass
321 321
322 322 filtertable = {
323 323 'tempfile:': tempfilter,
324 324 'pipe:': pipefilter,
325 325 }
326 326
327 327 def filter(s, cmd):
328 328 "filter a string through a command that transforms its input to its output"
329 329 for name, fn in filtertable.iteritems():
330 330 if cmd.startswith(name):
331 331 return fn(s, cmd[len(name):].lstrip())
332 332 return pipefilter(s, cmd)
333 333
334 334 def binary(s):
335 335 """return true if a string is binary data"""
336 336 return bool(s and '\0' in s)
337 337
338 338 def increasingchunks(source, min=1024, max=65536):
339 339 '''return no less than min bytes per chunk while data remains,
340 340 doubling min after each chunk until it reaches max'''
341 341 def log2(x):
342 342 if not x:
343 343 return 0
344 344 i = 0
345 345 while x:
346 346 x >>= 1
347 347 i += 1
348 348 return i - 1
349 349
350 350 buf = []
351 351 blen = 0
352 352 for chunk in source:
353 353 buf.append(chunk)
354 354 blen += len(chunk)
355 355 if blen >= min:
356 356 if min < max:
357 357 min = min << 1
358 358 nmin = 1 << log2(blen)
359 359 if nmin > min:
360 360 min = nmin
361 361 if min > max:
362 362 min = max
363 363 yield ''.join(buf)
364 364 blen = 0
365 365 buf = []
366 366 if buf:
367 367 yield ''.join(buf)
368 368
369 369 Abort = error.Abort
370 370
371 371 def always(fn):
372 372 return True
373 373
374 374 def never(fn):
375 375 return False
376 376
377 377 def pathto(root, n1, n2):
378 378 '''return the relative path from one place to another.
379 379 root should use os.sep to separate directories
380 380 n1 should use os.sep to separate directories
381 381 n2 should use "/" to separate directories
382 382 returns an os.sep-separated path.
383 383
384 384 If n1 is a relative path, it's assumed it's
385 385 relative to root.
386 386 n2 should always be relative to root.
387 387 '''
388 388 if not n1:
389 389 return localpath(n2)
390 390 if os.path.isabs(n1):
391 391 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
392 392 return os.path.join(root, localpath(n2))
393 393 n2 = '/'.join((pconvert(root), n2))
394 394 a, b = splitpath(n1), n2.split('/')
395 395 a.reverse()
396 396 b.reverse()
397 397 while a and b and a[-1] == b[-1]:
398 398 a.pop()
399 399 b.pop()
400 400 b.reverse()
401 401 return os.sep.join((['..'] * len(a)) + b) or '.'
402 402
403 403 _hgexecutable = None
404 404
405 405 def mainfrozen():
406 406 """return True if we are a frozen executable.
407 407
408 408 The code supports py2exe (most common, Windows only) and tools/freeze
409 409 (portable, not much used).
410 410 """
411 411 return (safehasattr(sys, "frozen") or # new py2exe
412 412 safehasattr(sys, "importers") or # old py2exe
413 413 imp.is_frozen("__main__")) # tools/freeze
414 414
415 415 def hgexecutable():
416 416 """return location of the 'hg' executable.
417 417
418 418 Defaults to $HG or 'hg' in the search path.
419 419 """
420 420 if _hgexecutable is None:
421 421 hg = os.environ.get('HG')
422 422 mainmod = sys.modules['__main__']
423 423 if hg:
424 424 _sethgexecutable(hg)
425 425 elif mainfrozen():
426 426 _sethgexecutable(sys.executable)
427 427 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
428 428 _sethgexecutable(mainmod.__file__)
429 429 else:
430 430 exe = findexe('hg') or os.path.basename(sys.argv[0])
431 431 _sethgexecutable(exe)
432 432 return _hgexecutable
433 433
434 434 def _sethgexecutable(path):
435 435 """set location of the 'hg' executable"""
436 436 global _hgexecutable
437 437 _hgexecutable = path
438 438
439 439 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None, out=None):
440 440 '''enhanced shell command execution.
441 441 run with environment maybe modified, maybe in different dir.
442 442
443 443 if command fails and onerr is None, return status. if ui object,
444 444 print error message and return status, else raise onerr object as
445 445 exception.
446 446
447 447 if out is specified, it is assumed to be a file-like object that has a
448 448 write() method. stdout and stderr will be redirected to out.'''
449 449 try:
450 450 sys.stdout.flush()
451 451 except Exception:
452 452 pass
453 453 def py2shell(val):
454 454 'convert python object into string that is useful to shell'
455 455 if val is None or val is False:
456 456 return '0'
457 457 if val is True:
458 458 return '1'
459 459 return str(val)
460 460 origcmd = cmd
461 461 cmd = quotecommand(cmd)
462 462 if sys.platform == 'plan9':
463 463 # subprocess kludge to work around issues in half-baked Python
464 464 # ports, notably bichued/python:
465 465 if not cwd is None:
466 466 os.chdir(cwd)
467 467 rc = os.system(cmd)
468 468 else:
469 469 env = dict(os.environ)
470 470 env.update((k, py2shell(v)) for k, v in environ.iteritems())
471 471 env['HG'] = hgexecutable()
472 472 if out is None or out == sys.__stdout__:
473 473 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
474 474 env=env, cwd=cwd)
475 475 else:
476 476 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
477 477 env=env, cwd=cwd, stdout=subprocess.PIPE,
478 478 stderr=subprocess.STDOUT)
479 479 for line in proc.stdout:
480 480 out.write(line)
481 481 proc.wait()
482 482 rc = proc.returncode
483 483 if sys.platform == 'OpenVMS' and rc & 1:
484 484 rc = 0
485 485 if rc and onerr:
486 486 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
487 487 explainexit(rc)[0])
488 488 if errprefix:
489 489 errmsg = '%s: %s' % (errprefix, errmsg)
490 490 try:
491 491 onerr.warn(errmsg + '\n')
492 492 except AttributeError:
493 493 raise onerr(errmsg)
494 494 return rc
495 495
496 496 def checksignature(func):
497 497 '''wrap a function with code to check for calling errors'''
498 498 def check(*args, **kwargs):
499 499 try:
500 500 return func(*args, **kwargs)
501 501 except TypeError:
502 502 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
503 503 raise error.SignatureError
504 504 raise
505 505
506 506 return check
507 507
508 508 def copyfile(src, dest):
509 509 "copy a file, preserving mode and atime/mtime"
510 510 if os.path.lexists(dest):
511 511 unlink(dest)
512 512 if os.path.islink(src):
513 513 os.symlink(os.readlink(src), dest)
514 514 else:
515 515 try:
516 516 shutil.copyfile(src, dest)
517 517 shutil.copymode(src, dest)
518 518 except shutil.Error, inst:
519 519 raise Abort(str(inst))
520 520
521 521 def copyfiles(src, dst, hardlink=None):
522 522 """Copy a directory tree using hardlinks if possible"""
523 523
524 524 if hardlink is None:
525 525 hardlink = (os.stat(src).st_dev ==
526 526 os.stat(os.path.dirname(dst)).st_dev)
527 527
528 528 num = 0
529 529 if os.path.isdir(src):
530 530 os.mkdir(dst)
531 531 for name, kind in osutil.listdir(src):
532 532 srcname = os.path.join(src, name)
533 533 dstname = os.path.join(dst, name)
534 534 hardlink, n = copyfiles(srcname, dstname, hardlink)
535 535 num += n
536 536 else:
537 537 if hardlink:
538 538 try:
539 539 oslink(src, dst)
540 540 except (IOError, OSError):
541 541 hardlink = False
542 542 shutil.copy(src, dst)
543 543 else:
544 544 shutil.copy(src, dst)
545 545 num += 1
546 546
547 547 return hardlink, num
548 548
549 549 _winreservednames = '''con prn aux nul
550 550 com1 com2 com3 com4 com5 com6 com7 com8 com9
551 551 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
552 552 _winreservedchars = ':*?"<>|'
553 553 def checkwinfilename(path):
554 554 '''Check that the base-relative path is a valid filename on Windows.
555 555 Returns None if the path is ok, or a UI string describing the problem.
556 556
557 557 >>> checkwinfilename("just/a/normal/path")
558 558 >>> checkwinfilename("foo/bar/con.xml")
559 559 "filename contains 'con', which is reserved on Windows"
560 560 >>> checkwinfilename("foo/con.xml/bar")
561 561 "filename contains 'con', which is reserved on Windows"
562 562 >>> checkwinfilename("foo/bar/xml.con")
563 563 >>> checkwinfilename("foo/bar/AUX/bla.txt")
564 564 "filename contains 'AUX', which is reserved on Windows"
565 565 >>> checkwinfilename("foo/bar/bla:.txt")
566 566 "filename contains ':', which is reserved on Windows"
567 567 >>> checkwinfilename("foo/bar/b\07la.txt")
568 568 "filename contains '\\\\x07', which is invalid on Windows"
569 569 >>> checkwinfilename("foo/bar/bla ")
570 570 "filename ends with ' ', which is not allowed on Windows"
571 571 >>> checkwinfilename("../bar")
572 572 '''
573 573 for n in path.replace('\\', '/').split('/'):
574 574 if not n:
575 575 continue
576 576 for c in n:
577 577 if c in _winreservedchars:
578 578 return _("filename contains '%s', which is reserved "
579 579 "on Windows") % c
580 580 if ord(c) <= 31:
581 581 return _("filename contains %r, which is invalid "
582 582 "on Windows") % c
583 583 base = n.split('.')[0]
584 584 if base and base.lower() in _winreservednames:
585 585 return _("filename contains '%s', which is reserved "
586 586 "on Windows") % base
587 587 t = n[-1]
588 588 if t in '. ' and n not in '..':
589 589 return _("filename ends with '%s', which is not allowed "
590 590 "on Windows") % t
591 591
592 592 if os.name == 'nt':
593 593 checkosfilename = checkwinfilename
594 594 else:
595 595 checkosfilename = platform.checkosfilename
596 596
597 597 def makelock(info, pathname):
598 598 try:
599 599 return os.symlink(info, pathname)
600 600 except OSError, why:
601 601 if why.errno == errno.EEXIST:
602 602 raise
603 603 except AttributeError: # no symlink in os
604 604 pass
605 605
606 606 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
607 607 os.write(ld, info)
608 608 os.close(ld)
609 609
610 610 def readlock(pathname):
611 611 try:
612 612 return os.readlink(pathname)
613 613 except OSError, why:
614 614 if why.errno not in (errno.EINVAL, errno.ENOSYS):
615 615 raise
616 616 except AttributeError: # no symlink in os
617 617 pass
618 618 fp = posixfile(pathname)
619 619 r = fp.read()
620 620 fp.close()
621 621 return r
622 622
623 623 def fstat(fp):
624 624 '''stat file object that may not have fileno method.'''
625 625 try:
626 626 return os.fstat(fp.fileno())
627 627 except AttributeError:
628 628 return os.stat(fp.name)
629 629
630 630 # File system features
631 631
632 632 def checkcase(path):
633 633 """
634 634 Check whether the given path is on a case-sensitive filesystem
635 635
636 636 Requires a path (like /foo/.hg) ending with a foldable final
637 637 directory component.
638 638 """
639 639 s1 = os.stat(path)
640 640 d, b = os.path.split(path)
641 641 b2 = b.upper()
642 642 if b == b2:
643 643 b2 = b.lower()
644 644 if b == b2:
645 645 return True # no evidence against case sensitivity
646 646 p2 = os.path.join(d, b2)
647 647 try:
648 648 s2 = os.stat(p2)
649 649 if s2 == s1:
650 650 return False
651 651 return True
652 652 except OSError:
653 653 return True
654 654
655 655 try:
656 656 import re2
657 657 _re2 = None
658 658 except ImportError:
659 659 _re2 = False
660 660
661 661 def compilere(pat):
662 662 '''Compile a regular expression, using re2 if possible
663 663
664 664 For best performance, use only re2-compatible regexp features.'''
665 665 global _re2
666 666 if _re2 is None:
667 667 try:
668 668 re2.compile
669 669 _re2 = True
670 670 except ImportError:
671 671 _re2 = False
672 672 if _re2:
673 673 try:
674 674 return re2.compile(pat)
675 675 except re2.error:
676 676 pass
677 677 return re.compile(pat)
678 678
679 679 _fspathcache = {}
680 680 def fspath(name, root):
681 681 '''Get name in the case stored in the filesystem
682 682
683 683 The name should be relative to root, and be normcase-ed for efficiency.
684 684
685 685 Note that this function is unnecessary, and should not be
686 686 called, for case-sensitive filesystems (simply because it's expensive).
687 687
688 688 The root should be normcase-ed, too.
689 689 '''
690 690 def find(p, contents):
691 691 for n in contents:
692 692 if normcase(n) == p:
693 693 return n
694 694 return None
695 695
696 696 seps = os.sep
697 697 if os.altsep:
698 698 seps = seps + os.altsep
699 699 # Protect backslashes. This gets silly very quickly.
700 700 seps.replace('\\','\\\\')
701 701 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
702 702 dir = os.path.normpath(root)
703 703 result = []
704 704 for part, sep in pattern.findall(name):
705 705 if sep:
706 706 result.append(sep)
707 707 continue
708 708
709 709 if dir not in _fspathcache:
710 710 _fspathcache[dir] = os.listdir(dir)
711 711 contents = _fspathcache[dir]
712 712
713 713 found = find(part, contents)
714 714 if not found:
715 715 # retry "once per directory" per "dirstate.walk" which
716 716 # may take place for each patches of "hg qpush", for example
717 717 contents = os.listdir(dir)
718 718 _fspathcache[dir] = contents
719 719 found = find(part, contents)
720 720
721 721 result.append(found or part)
722 722 dir = os.path.join(dir, part)
723 723
724 724 return ''.join(result)
725 725
726 726 def checknlink(testfile):
727 727 '''check whether hardlink count reporting works properly'''
728 728
729 729 # testfile may be open, so we need a separate file for checking to
730 730 # work around issue2543 (or testfile may get lost on Samba shares)
731 731 f1 = testfile + ".hgtmp1"
732 732 if os.path.lexists(f1):
733 733 return False
734 734 try:
735 735 posixfile(f1, 'w').close()
736 736 except IOError:
737 737 return False
738 738
739 739 f2 = testfile + ".hgtmp2"
740 740 fd = None
741 741 try:
742 742 try:
743 743 oslink(f1, f2)
744 744 except OSError:
745 745 return False
746 746
747 747 # nlinks() may behave differently for files on Windows shares if
748 748 # the file is open.
749 749 fd = posixfile(f2)
750 750 return nlinks(f2) > 1
751 751 finally:
752 752 if fd is not None:
753 753 fd.close()
754 754 for f in (f1, f2):
755 755 try:
756 756 os.unlink(f)
757 757 except OSError:
758 758 pass
759 759
760 760 return False
761 761
762 762 def endswithsep(path):
763 763 '''Check path ends with os.sep or os.altsep.'''
764 764 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
765 765
766 766 def splitpath(path):
767 767 '''Split path by os.sep.
768 768 Note that this function does not use os.altsep because this is
769 769 an alternative of simple "xxx.split(os.sep)".
770 770 It is recommended to use os.path.normpath() before using this
771 771 function if need.'''
772 772 return path.split(os.sep)
773 773
774 774 def gui():
775 775 '''Are we running in a GUI?'''
776 776 if sys.platform == 'darwin':
777 777 if 'SSH_CONNECTION' in os.environ:
778 778 # handle SSH access to a box where the user is logged in
779 779 return False
780 780 elif getattr(osutil, 'isgui', None):
781 781 # check if a CoreGraphics session is available
782 782 return osutil.isgui()
783 783 else:
784 784 # pure build; use a safe default
785 785 return True
786 786 else:
787 787 return os.name == "nt" or os.environ.get("DISPLAY")
788 788
789 789 def mktempcopy(name, emptyok=False, createmode=None):
790 790 """Create a temporary file with the same contents from name
791 791
792 792 The permission bits are copied from the original file.
793 793
794 794 If the temporary file is going to be truncated immediately, you
795 795 can use emptyok=True as an optimization.
796 796
797 797 Returns the name of the temporary file.
798 798 """
799 799 d, fn = os.path.split(name)
800 800 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
801 801 os.close(fd)
802 802 # Temporary files are created with mode 0600, which is usually not
803 803 # what we want. If the original file already exists, just copy
804 804 # its mode. Otherwise, manually obey umask.
805 805 copymode(name, temp, createmode)
806 806 if emptyok:
807 807 return temp
808 808 try:
809 809 try:
810 810 ifp = posixfile(name, "rb")
811 811 except IOError, inst:
812 812 if inst.errno == errno.ENOENT:
813 813 return temp
814 814 if not getattr(inst, 'filename', None):
815 815 inst.filename = name
816 816 raise
817 817 ofp = posixfile(temp, "wb")
818 818 for chunk in filechunkiter(ifp):
819 819 ofp.write(chunk)
820 820 ifp.close()
821 821 ofp.close()
822 822 except: # re-raises
823 823 try: os.unlink(temp)
824 824 except OSError: pass
825 825 raise
826 826 return temp
827 827
828 828 class atomictempfile(object):
829 829 '''writable file object that atomically updates a file
830 830
831 831 All writes will go to a temporary copy of the original file. Call
832 832 close() when you are done writing, and atomictempfile will rename
833 833 the temporary copy to the original name, making the changes
834 834 visible. If the object is destroyed without being closed, all your
835 835 writes are discarded.
836 836 '''
837 837 def __init__(self, name, mode='w+b', createmode=None):
838 838 self.__name = name # permanent name
839 839 self._tempname = mktempcopy(name, emptyok=('w' in mode),
840 840 createmode=createmode)
841 841 self._fp = posixfile(self._tempname, mode)
842 842
843 843 # delegated methods
844 844 self.write = self._fp.write
845 845 self.seek = self._fp.seek
846 846 self.tell = self._fp.tell
847 847 self.fileno = self._fp.fileno
848 848
849 849 def close(self):
850 850 if not self._fp.closed:
851 851 self._fp.close()
852 852 rename(self._tempname, localpath(self.__name))
853 853
854 854 def discard(self):
855 855 if not self._fp.closed:
856 856 try:
857 857 os.unlink(self._tempname)
858 858 except OSError:
859 859 pass
860 860 self._fp.close()
861 861
862 862 def __del__(self):
863 863 if safehasattr(self, '_fp'): # constructor actually did something
864 864 self.discard()
865 865
866 866 def makedirs(name, mode=None):
867 867 """recursive directory creation with parent mode inheritance"""
868 868 try:
869 869 os.mkdir(name)
870 870 except OSError, err:
871 871 if err.errno == errno.EEXIST:
872 872 return
873 873 if err.errno != errno.ENOENT or not name:
874 874 raise
875 875 parent = os.path.dirname(os.path.abspath(name))
876 876 if parent == name:
877 877 raise
878 878 makedirs(parent, mode)
879 879 os.mkdir(name)
880 880 if mode is not None:
881 881 os.chmod(name, mode)
882 882
883 883 def ensuredirs(name, mode=None):
884 884 """race-safe recursive directory creation"""
885 885 if os.path.isdir(name):
886 886 return
887 887 parent = os.path.dirname(os.path.abspath(name))
888 888 if parent != name:
889 889 ensuredirs(parent, mode)
890 890 try:
891 891 os.mkdir(name)
892 892 except OSError, err:
893 893 if err.errno == errno.EEXIST and os.path.isdir(name):
894 894 # someone else seems to have won a directory creation race
895 895 return
896 896 raise
897 897 if mode is not None:
898 898 os.chmod(name, mode)
899 899
900 900 def readfile(path):
901 901 fp = open(path, 'rb')
902 902 try:
903 903 return fp.read()
904 904 finally:
905 905 fp.close()
906 906
907 907 def writefile(path, text):
908 908 fp = open(path, 'wb')
909 909 try:
910 910 fp.write(text)
911 911 finally:
912 912 fp.close()
913 913
914 914 def appendfile(path, text):
915 915 fp = open(path, 'ab')
916 916 try:
917 917 fp.write(text)
918 918 finally:
919 919 fp.close()
920 920
921 921 class chunkbuffer(object):
922 922 """Allow arbitrary sized chunks of data to be efficiently read from an
923 923 iterator over chunks of arbitrary size."""
924 924
925 925 def __init__(self, in_iter):
926 926 """in_iter is the iterator that's iterating over the input chunks.
927 927 targetsize is how big a buffer to try to maintain."""
928 928 def splitbig(chunks):
929 929 for chunk in chunks:
930 930 if len(chunk) > 2**20:
931 931 pos = 0
932 932 while pos < len(chunk):
933 933 end = pos + 2 ** 18
934 934 yield chunk[pos:end]
935 935 pos = end
936 936 else:
937 937 yield chunk
938 938 self.iter = splitbig(in_iter)
939 939 self._queue = deque()
940 940
941 941 def read(self, l):
942 942 """Read L bytes of data from the iterator of chunks of data.
943 943 Returns less than L bytes if the iterator runs dry."""
944 944 left = l
945 945 buf = []
946 946 queue = self._queue
947 947 while left > 0:
948 948 # refill the queue
949 949 if not queue:
950 950 target = 2**18
951 951 for chunk in self.iter:
952 952 queue.append(chunk)
953 953 target -= len(chunk)
954 954 if target <= 0:
955 955 break
956 956 if not queue:
957 957 break
958 958
959 959 chunk = queue.popleft()
960 960 left -= len(chunk)
961 961 if left < 0:
962 962 queue.appendleft(chunk[left:])
963 963 buf.append(chunk[:left])
964 964 else:
965 965 buf.append(chunk)
966 966
967 967 return ''.join(buf)
968 968
969 969 def filechunkiter(f, size=65536, limit=None):
970 970 """Create a generator that produces the data in the file size
971 971 (default 65536) bytes at a time, up to optional limit (default is
972 972 to read all data). Chunks may be less than size bytes if the
973 973 chunk is the last chunk in the file, or the file is a socket or
974 974 some other type of file that sometimes reads less data than is
975 975 requested."""
976 976 assert size >= 0
977 977 assert limit is None or limit >= 0
978 978 while True:
979 979 if limit is None:
980 980 nbytes = size
981 981 else:
982 982 nbytes = min(limit, size)
983 983 s = nbytes and f.read(nbytes)
984 984 if not s:
985 985 break
986 986 if limit:
987 987 limit -= len(s)
988 988 yield s
989 989
990 990 def makedate():
991 991 ct = time.time()
992 992 if ct < 0:
993 993 hint = _("check your clock")
994 994 raise Abort(_("negative timestamp: %d") % ct, hint=hint)
995 995 delta = (datetime.datetime.utcfromtimestamp(ct) -
996 996 datetime.datetime.fromtimestamp(ct))
997 997 tz = delta.days * 86400 + delta.seconds
998 998 return ct, tz
999 999
1000 1000 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1001 1001 """represent a (unixtime, offset) tuple as a localized time.
1002 1002 unixtime is seconds since the epoch, and offset is the time zone's
1003 1003 number of seconds away from UTC. if timezone is false, do not
1004 1004 append time zone to string."""
1005 1005 t, tz = date or makedate()
1006 1006 if t < 0:
1007 1007 t = 0 # time.gmtime(lt) fails on Windows for lt < -43200
1008 1008 tz = 0
1009 1009 if "%1" in format or "%2" in format:
1010 1010 sign = (tz > 0) and "-" or "+"
1011 1011 minutes = abs(tz) // 60
1012 1012 format = format.replace("%1", "%c%02d" % (sign, minutes // 60))
1013 1013 format = format.replace("%2", "%02d" % (minutes % 60))
1014 1014 try:
1015 1015 t = time.gmtime(float(t) - tz)
1016 1016 except ValueError:
1017 1017 # time was out of range
1018 1018 t = time.gmtime(sys.maxint)
1019 1019 s = time.strftime(format, t)
1020 1020 return s
1021 1021
1022 1022 def shortdate(date=None):
1023 1023 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1024 1024 return datestr(date, format='%Y-%m-%d')
1025 1025
1026 1026 def strdate(string, format, defaults=[]):
1027 1027 """parse a localized time string and return a (unixtime, offset) tuple.
1028 1028 if the string cannot be parsed, ValueError is raised."""
1029 1029 def timezone(string):
1030 1030 tz = string.split()[-1]
1031 1031 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1032 1032 sign = (tz[0] == "+") and 1 or -1
1033 1033 hours = int(tz[1:3])
1034 1034 minutes = int(tz[3:5])
1035 1035 return -sign * (hours * 60 + minutes) * 60
1036 1036 if tz == "GMT" or tz == "UTC":
1037 1037 return 0
1038 1038 return None
1039 1039
1040 1040 # NOTE: unixtime = localunixtime + offset
1041 1041 offset, date = timezone(string), string
1042 1042 if offset is not None:
1043 1043 date = " ".join(string.split()[:-1])
1044 1044
1045 1045 # add missing elements from defaults
1046 1046 usenow = False # default to using biased defaults
1047 1047 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1048 1048 found = [True for p in part if ("%"+p) in format]
1049 1049 if not found:
1050 1050 date += "@" + defaults[part][usenow]
1051 1051 format += "@%" + part[0]
1052 1052 else:
1053 1053 # We've found a specific time element, less specific time
1054 1054 # elements are relative to today
1055 1055 usenow = True
1056 1056
1057 1057 timetuple = time.strptime(date, format)
1058 1058 localunixtime = int(calendar.timegm(timetuple))
1059 1059 if offset is None:
1060 1060 # local timezone
1061 1061 unixtime = int(time.mktime(timetuple))
1062 1062 offset = unixtime - localunixtime
1063 1063 else:
1064 1064 unixtime = localunixtime + offset
1065 1065 return unixtime, offset
1066 1066
1067 1067 def parsedate(date, formats=None, bias={}):
1068 1068 """parse a localized date/time and return a (unixtime, offset) tuple.
1069 1069
1070 1070 The date may be a "unixtime offset" string or in one of the specified
1071 1071 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1072 1072
1073 1073 >>> parsedate(' today ') == parsedate(\
1074 1074 datetime.date.today().strftime('%b %d'))
1075 1075 True
1076 1076 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1077 1077 datetime.timedelta(days=1)\
1078 1078 ).strftime('%b %d'))
1079 1079 True
1080 1080 >>> now, tz = makedate()
1081 1081 >>> strnow, strtz = parsedate('now')
1082 1082 >>> (strnow - now) < 1
1083 1083 True
1084 1084 >>> tz == strtz
1085 1085 True
1086 1086 """
1087 1087 if not date:
1088 1088 return 0, 0
1089 1089 if isinstance(date, tuple) and len(date) == 2:
1090 1090 return date
1091 1091 if not formats:
1092 1092 formats = defaultdateformats
1093 1093 date = date.strip()
1094 1094
1095 1095 if date == _('now'):
1096 1096 return makedate()
1097 1097 if date == _('today'):
1098 1098 date = datetime.date.today().strftime('%b %d')
1099 1099 elif date == _('yesterday'):
1100 1100 date = (datetime.date.today() -
1101 1101 datetime.timedelta(days=1)).strftime('%b %d')
1102 1102
1103 1103 try:
1104 1104 when, offset = map(int, date.split(' '))
1105 1105 except ValueError:
1106 1106 # fill out defaults
1107 1107 now = makedate()
1108 1108 defaults = {}
1109 1109 for part in ("d", "mb", "yY", "HI", "M", "S"):
1110 1110 # this piece is for rounding the specific end of unknowns
1111 1111 b = bias.get(part)
1112 1112 if b is None:
1113 1113 if part[0] in "HMS":
1114 1114 b = "00"
1115 1115 else:
1116 1116 b = "0"
1117 1117
1118 1118 # this piece is for matching the generic end to today's date
1119 1119 n = datestr(now, "%" + part[0])
1120 1120
1121 1121 defaults[part] = (b, n)
1122 1122
1123 1123 for format in formats:
1124 1124 try:
1125 1125 when, offset = strdate(date, format, defaults)
1126 1126 except (ValueError, OverflowError):
1127 1127 pass
1128 1128 else:
1129 1129 break
1130 1130 else:
1131 1131 raise Abort(_('invalid date: %r') % date)
1132 1132 # validate explicit (probably user-specified) date and
1133 1133 # time zone offset. values must fit in signed 32 bits for
1134 1134 # current 32-bit linux runtimes. timezones go from UTC-12
1135 1135 # to UTC+14
1136 1136 if abs(when) > 0x7fffffff:
1137 1137 raise Abort(_('date exceeds 32 bits: %d') % when)
1138 1138 if when < 0:
1139 1139 raise Abort(_('negative date value: %d') % when)
1140 1140 if offset < -50400 or offset > 43200:
1141 1141 raise Abort(_('impossible time zone offset: %d') % offset)
1142 1142 return when, offset
1143 1143
1144 1144 def matchdate(date):
1145 1145 """Return a function that matches a given date match specifier
1146 1146
1147 1147 Formats include:
1148 1148
1149 1149 '{date}' match a given date to the accuracy provided
1150 1150
1151 1151 '<{date}' on or before a given date
1152 1152
1153 1153 '>{date}' on or after a given date
1154 1154
1155 1155 >>> p1 = parsedate("10:29:59")
1156 1156 >>> p2 = parsedate("10:30:00")
1157 1157 >>> p3 = parsedate("10:30:59")
1158 1158 >>> p4 = parsedate("10:31:00")
1159 1159 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1160 1160 >>> f = matchdate("10:30")
1161 1161 >>> f(p1[0])
1162 1162 False
1163 1163 >>> f(p2[0])
1164 1164 True
1165 1165 >>> f(p3[0])
1166 1166 True
1167 1167 >>> f(p4[0])
1168 1168 False
1169 1169 >>> f(p5[0])
1170 1170 False
1171 1171 """
1172 1172
1173 1173 def lower(date):
1174 1174 d = dict(mb="1", d="1")
1175 1175 return parsedate(date, extendeddateformats, d)[0]
1176 1176
1177 1177 def upper(date):
1178 1178 d = dict(mb="12", HI="23", M="59", S="59")
1179 1179 for days in ("31", "30", "29"):
1180 1180 try:
1181 1181 d["d"] = days
1182 1182 return parsedate(date, extendeddateformats, d)[0]
1183 1183 except Abort:
1184 1184 pass
1185 1185 d["d"] = "28"
1186 1186 return parsedate(date, extendeddateformats, d)[0]
1187 1187
1188 1188 date = date.strip()
1189 1189
1190 1190 if not date:
1191 1191 raise Abort(_("dates cannot consist entirely of whitespace"))
1192 1192 elif date[0] == "<":
1193 1193 if not date[1:]:
1194 1194 raise Abort(_("invalid day spec, use '<DATE'"))
1195 1195 when = upper(date[1:])
1196 1196 return lambda x: x <= when
1197 1197 elif date[0] == ">":
1198 1198 if not date[1:]:
1199 1199 raise Abort(_("invalid day spec, use '>DATE'"))
1200 1200 when = lower(date[1:])
1201 1201 return lambda x: x >= when
1202 1202 elif date[0] == "-":
1203 1203 try:
1204 1204 days = int(date[1:])
1205 1205 except ValueError:
1206 1206 raise Abort(_("invalid day spec: %s") % date[1:])
1207 1207 if days < 0:
1208 1208 raise Abort(_("%s must be nonnegative (see 'hg help dates')")
1209 1209 % date[1:])
1210 1210 when = makedate()[0] - days * 3600 * 24
1211 1211 return lambda x: x >= when
1212 1212 elif " to " in date:
1213 1213 a, b = date.split(" to ")
1214 1214 start, stop = lower(a), upper(b)
1215 1215 return lambda x: x >= start and x <= stop
1216 1216 else:
1217 1217 start, stop = lower(date), upper(date)
1218 1218 return lambda x: x >= start and x <= stop
1219 1219
1220 1220 def shortuser(user):
1221 1221 """Return a short representation of a user name or email address."""
1222 1222 f = user.find('@')
1223 1223 if f >= 0:
1224 1224 user = user[:f]
1225 1225 f = user.find('<')
1226 1226 if f >= 0:
1227 1227 user = user[f + 1:]
1228 1228 f = user.find(' ')
1229 1229 if f >= 0:
1230 1230 user = user[:f]
1231 1231 f = user.find('.')
1232 1232 if f >= 0:
1233 1233 user = user[:f]
1234 1234 return user
1235 1235
1236 1236 def emailuser(user):
1237 1237 """Return the user portion of an email address."""
1238 1238 f = user.find('@')
1239 1239 if f >= 0:
1240 1240 user = user[:f]
1241 1241 f = user.find('<')
1242 1242 if f >= 0:
1243 1243 user = user[f + 1:]
1244 1244 return user
1245 1245
1246 1246 def email(author):
1247 1247 '''get email of author.'''
1248 1248 r = author.find('>')
1249 1249 if r == -1:
1250 1250 r = None
1251 1251 return author[author.find('<') + 1:r]
1252 1252
1253 1253 def _ellipsis(text, maxlength):
1254 1254 if len(text) <= maxlength:
1255 1255 return text, False
1256 1256 else:
1257 1257 return "%s..." % (text[:maxlength - 3]), True
1258 1258
1259 1259 def ellipsis(text, maxlength=400):
1260 1260 """Trim string to at most maxlength (default: 400) characters."""
1261 1261 try:
1262 1262 # use unicode not to split at intermediate multi-byte sequence
1263 1263 utext, truncated = _ellipsis(text.decode(encoding.encoding),
1264 1264 maxlength)
1265 1265 if not truncated:
1266 1266 return text
1267 1267 return utext.encode(encoding.encoding)
1268 1268 except (UnicodeDecodeError, UnicodeEncodeError):
1269 1269 return _ellipsis(text, maxlength)[0]
1270 1270
1271 1271 def unitcountfn(*unittable):
1272 1272 '''return a function that renders a readable count of some quantity'''
1273 1273
1274 1274 def go(count):
1275 1275 for multiplier, divisor, format in unittable:
1276 1276 if count >= divisor * multiplier:
1277 1277 return format % (count / float(divisor))
1278 1278 return unittable[-1][2] % count
1279 1279
1280 1280 return go
1281 1281
1282 1282 bytecount = unitcountfn(
1283 1283 (100, 1 << 30, _('%.0f GB')),
1284 1284 (10, 1 << 30, _('%.1f GB')),
1285 1285 (1, 1 << 30, _('%.2f GB')),
1286 1286 (100, 1 << 20, _('%.0f MB')),
1287 1287 (10, 1 << 20, _('%.1f MB')),
1288 1288 (1, 1 << 20, _('%.2f MB')),
1289 1289 (100, 1 << 10, _('%.0f KB')),
1290 1290 (10, 1 << 10, _('%.1f KB')),
1291 1291 (1, 1 << 10, _('%.2f KB')),
1292 1292 (1, 1, _('%.0f bytes')),
1293 1293 )
1294 1294
1295 1295 def uirepr(s):
1296 1296 # Avoid double backslash in Windows path repr()
1297 1297 return repr(s).replace('\\\\', '\\')
1298 1298
1299 1299 # delay import of textwrap
1300 1300 def MBTextWrapper(**kwargs):
1301 1301 class tw(textwrap.TextWrapper):
1302 1302 """
1303 1303 Extend TextWrapper for width-awareness.
1304 1304
1305 1305 Neither number of 'bytes' in any encoding nor 'characters' is
1306 1306 appropriate to calculate terminal columns for specified string.
1307 1307
1308 1308 Original TextWrapper implementation uses built-in 'len()' directly,
1309 1309 so overriding is needed to use width information of each characters.
1310 1310
1311 1311 In addition, characters classified into 'ambiguous' width are
1312 1312 treated as wide in East Asian area, but as narrow in other.
1313 1313
1314 1314 This requires use decision to determine width of such characters.
1315 1315 """
1316 1316 def __init__(self, **kwargs):
1317 1317 textwrap.TextWrapper.__init__(self, **kwargs)
1318 1318
1319 1319 # for compatibility between 2.4 and 2.6
1320 1320 if getattr(self, 'drop_whitespace', None) is None:
1321 1321 self.drop_whitespace = kwargs.get('drop_whitespace', True)
1322 1322
1323 1323 def _cutdown(self, ucstr, space_left):
1324 1324 l = 0
1325 1325 colwidth = encoding.ucolwidth
1326 1326 for i in xrange(len(ucstr)):
1327 1327 l += colwidth(ucstr[i])
1328 1328 if space_left < l:
1329 1329 return (ucstr[:i], ucstr[i:])
1330 1330 return ucstr, ''
1331 1331
1332 1332 # overriding of base class
1333 1333 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1334 1334 space_left = max(width - cur_len, 1)
1335 1335
1336 1336 if self.break_long_words:
1337 1337 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1338 1338 cur_line.append(cut)
1339 1339 reversed_chunks[-1] = res
1340 1340 elif not cur_line:
1341 1341 cur_line.append(reversed_chunks.pop())
1342 1342
1343 1343 # this overriding code is imported from TextWrapper of python 2.6
1344 1344 # to calculate columns of string by 'encoding.ucolwidth()'
1345 1345 def _wrap_chunks(self, chunks):
1346 1346 colwidth = encoding.ucolwidth
1347 1347
1348 1348 lines = []
1349 1349 if self.width <= 0:
1350 1350 raise ValueError("invalid width %r (must be > 0)" % self.width)
1351 1351
1352 1352 # Arrange in reverse order so items can be efficiently popped
1353 1353 # from a stack of chucks.
1354 1354 chunks.reverse()
1355 1355
1356 1356 while chunks:
1357 1357
1358 1358 # Start the list of chunks that will make up the current line.
1359 1359 # cur_len is just the length of all the chunks in cur_line.
1360 1360 cur_line = []
1361 1361 cur_len = 0
1362 1362
1363 1363 # Figure out which static string will prefix this line.
1364 1364 if lines:
1365 1365 indent = self.subsequent_indent
1366 1366 else:
1367 1367 indent = self.initial_indent
1368 1368
1369 1369 # Maximum width for this line.
1370 1370 width = self.width - len(indent)
1371 1371
1372 1372 # First chunk on line is whitespace -- drop it, unless this
1373 1373 # is the very beginning of the text (i.e. no lines started yet).
1374 1374 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
1375 1375 del chunks[-1]
1376 1376
1377 1377 while chunks:
1378 1378 l = colwidth(chunks[-1])
1379 1379
1380 1380 # Can at least squeeze this chunk onto the current line.
1381 1381 if cur_len + l <= width:
1382 1382 cur_line.append(chunks.pop())
1383 1383 cur_len += l
1384 1384
1385 1385 # Nope, this line is full.
1386 1386 else:
1387 1387 break
1388 1388
1389 1389 # The current line is full, and the next chunk is too big to
1390 1390 # fit on *any* line (not just this one).
1391 1391 if chunks and colwidth(chunks[-1]) > width:
1392 1392 self._handle_long_word(chunks, cur_line, cur_len, width)
1393 1393
1394 1394 # If the last chunk on this line is all whitespace, drop it.
1395 1395 if (self.drop_whitespace and
1396 1396 cur_line and cur_line[-1].strip() == ''):
1397 1397 del cur_line[-1]
1398 1398
1399 1399 # Convert current line back to a string and store it in list
1400 1400 # of all lines (return value).
1401 1401 if cur_line:
1402 1402 lines.append(indent + ''.join(cur_line))
1403 1403
1404 1404 return lines
1405 1405
1406 1406 global MBTextWrapper
1407 1407 MBTextWrapper = tw
1408 1408 return tw(**kwargs)
1409 1409
1410 1410 def wrap(line, width, initindent='', hangindent=''):
1411 1411 maxindent = max(len(hangindent), len(initindent))
1412 1412 if width <= maxindent:
1413 1413 # adjust for weird terminal size
1414 1414 width = max(78, maxindent + 1)
1415 1415 line = line.decode(encoding.encoding, encoding.encodingmode)
1416 1416 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
1417 1417 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
1418 1418 wrapper = MBTextWrapper(width=width,
1419 1419 initial_indent=initindent,
1420 1420 subsequent_indent=hangindent)
1421 1421 return wrapper.fill(line).encode(encoding.encoding)
1422 1422
1423 1423 def iterlines(iterator):
1424 1424 for chunk in iterator:
1425 1425 for line in chunk.splitlines():
1426 1426 yield line
1427 1427
1428 1428 def expandpath(path):
1429 1429 return os.path.expanduser(os.path.expandvars(path))
1430 1430
1431 1431 def hgcmd():
1432 1432 """Return the command used to execute current hg
1433 1433
1434 1434 This is different from hgexecutable() because on Windows we want
1435 1435 to avoid things opening new shell windows like batch files, so we
1436 1436 get either the python call or current executable.
1437 1437 """
1438 1438 if mainfrozen():
1439 1439 return [sys.executable]
1440 1440 return gethgcmd()
1441 1441
1442 1442 def rundetached(args, condfn):
1443 1443 """Execute the argument list in a detached process.
1444 1444
1445 1445 condfn is a callable which is called repeatedly and should return
1446 1446 True once the child process is known to have started successfully.
1447 1447 At this point, the child process PID is returned. If the child
1448 1448 process fails to start or finishes before condfn() evaluates to
1449 1449 True, return -1.
1450 1450 """
1451 1451 # Windows case is easier because the child process is either
1452 1452 # successfully starting and validating the condition or exiting
1453 1453 # on failure. We just poll on its PID. On Unix, if the child
1454 1454 # process fails to start, it will be left in a zombie state until
1455 1455 # the parent wait on it, which we cannot do since we expect a long
1456 1456 # running process on success. Instead we listen for SIGCHLD telling
1457 1457 # us our child process terminated.
1458 1458 terminated = set()
1459 1459 def handler(signum, frame):
1460 1460 terminated.add(os.wait())
1461 1461 prevhandler = None
1462 1462 SIGCHLD = getattr(signal, 'SIGCHLD', None)
1463 1463 if SIGCHLD is not None:
1464 1464 prevhandler = signal.signal(SIGCHLD, handler)
1465 1465 try:
1466 1466 pid = spawndetached(args)
1467 1467 while not condfn():
1468 1468 if ((pid in terminated or not testpid(pid))
1469 1469 and not condfn()):
1470 1470 return -1
1471 1471 time.sleep(0.1)
1472 1472 return pid
1473 1473 finally:
1474 1474 if prevhandler is not None:
1475 1475 signal.signal(signal.SIGCHLD, prevhandler)
1476 1476
1477 1477 try:
1478 1478 any, all = any, all
1479 1479 except NameError:
1480 1480 def any(iterable):
1481 1481 for i in iterable:
1482 1482 if i:
1483 1483 return True
1484 1484 return False
1485 1485
1486 1486 def all(iterable):
1487 1487 for i in iterable:
1488 1488 if not i:
1489 1489 return False
1490 1490 return True
1491 1491
1492 1492 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
1493 1493 """Return the result of interpolating items in the mapping into string s.
1494 1494
1495 1495 prefix is a single character string, or a two character string with
1496 1496 a backslash as the first character if the prefix needs to be escaped in
1497 1497 a regular expression.
1498 1498
1499 1499 fn is an optional function that will be applied to the replacement text
1500 1500 just before replacement.
1501 1501
1502 1502 escape_prefix is an optional flag that allows using doubled prefix for
1503 1503 its escaping.
1504 1504 """
1505 1505 fn = fn or (lambda s: s)
1506 1506 patterns = '|'.join(mapping.keys())
1507 1507 if escape_prefix:
1508 1508 patterns += '|' + prefix
1509 1509 if len(prefix) > 1:
1510 1510 prefix_char = prefix[1:]
1511 1511 else:
1512 1512 prefix_char = prefix
1513 1513 mapping[prefix_char] = prefix_char
1514 1514 r = re.compile(r'%s(%s)' % (prefix, patterns))
1515 1515 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
1516 1516
1517 1517 def getport(port):
1518 1518 """Return the port for a given network service.
1519 1519
1520 1520 If port is an integer, it's returned as is. If it's a string, it's
1521 1521 looked up using socket.getservbyname(). If there's no matching
1522 1522 service, util.Abort is raised.
1523 1523 """
1524 1524 try:
1525 1525 return int(port)
1526 1526 except ValueError:
1527 1527 pass
1528 1528
1529 1529 try:
1530 1530 return socket.getservbyname(port)
1531 1531 except socket.error:
1532 1532 raise Abort(_("no port number associated with service '%s'") % port)
1533 1533
1534 1534 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
1535 1535 '0': False, 'no': False, 'false': False, 'off': False,
1536 1536 'never': False}
1537 1537
1538 1538 def parsebool(s):
1539 1539 """Parse s into a boolean.
1540 1540
1541 1541 If s is not a valid boolean, returns None.
1542 1542 """
1543 1543 return _booleans.get(s.lower(), None)
1544 1544
1545 1545 _hexdig = '0123456789ABCDEFabcdef'
1546 1546 _hextochr = dict((a + b, chr(int(a + b, 16)))
1547 1547 for a in _hexdig for b in _hexdig)
1548 1548
1549 1549 def _urlunquote(s):
1550 1550 """Decode HTTP/HTML % encoding.
1551 1551
1552 1552 >>> _urlunquote('abc%20def')
1553 1553 'abc def'
1554 1554 """
1555 1555 res = s.split('%')
1556 1556 # fastpath
1557 1557 if len(res) == 1:
1558 1558 return s
1559 1559 s = res[0]
1560 1560 for item in res[1:]:
1561 1561 try:
1562 1562 s += _hextochr[item[:2]] + item[2:]
1563 1563 except KeyError:
1564 1564 s += '%' + item
1565 1565 except UnicodeDecodeError:
1566 1566 s += unichr(int(item[:2], 16)) + item[2:]
1567 1567 return s
1568 1568
1569 1569 class url(object):
1570 1570 r"""Reliable URL parser.
1571 1571
1572 1572 This parses URLs and provides attributes for the following
1573 1573 components:
1574 1574
1575 1575 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
1576 1576
1577 1577 Missing components are set to None. The only exception is
1578 1578 fragment, which is set to '' if present but empty.
1579 1579
1580 1580 If parsefragment is False, fragment is included in query. If
1581 1581 parsequery is False, query is included in path. If both are
1582 1582 False, both fragment and query are included in path.
1583 1583
1584 1584 See http://www.ietf.org/rfc/rfc2396.txt for more information.
1585 1585
1586 1586 Note that for backward compatibility reasons, bundle URLs do not
1587 1587 take host names. That means 'bundle://../' has a path of '../'.
1588 1588
1589 1589 Examples:
1590 1590
1591 1591 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
1592 1592 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
1593 1593 >>> url('ssh://[::1]:2200//home/joe/repo')
1594 1594 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
1595 1595 >>> url('file:///home/joe/repo')
1596 1596 <url scheme: 'file', path: '/home/joe/repo'>
1597 1597 >>> url('file:///c:/temp/foo/')
1598 1598 <url scheme: 'file', path: 'c:/temp/foo/'>
1599 1599 >>> url('bundle:foo')
1600 1600 <url scheme: 'bundle', path: 'foo'>
1601 1601 >>> url('bundle://../foo')
1602 1602 <url scheme: 'bundle', path: '../foo'>
1603 1603 >>> url(r'c:\foo\bar')
1604 1604 <url path: 'c:\\foo\\bar'>
1605 1605 >>> url(r'\\blah\blah\blah')
1606 1606 <url path: '\\\\blah\\blah\\blah'>
1607 1607 >>> url(r'\\blah\blah\blah#baz')
1608 1608 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
1609 1609
1610 1610 Authentication credentials:
1611 1611
1612 1612 >>> url('ssh://joe:xyz@x/repo')
1613 1613 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
1614 1614 >>> url('ssh://joe@x/repo')
1615 1615 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
1616 1616
1617 1617 Query strings and fragments:
1618 1618
1619 1619 >>> url('http://host/a?b#c')
1620 1620 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
1621 1621 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
1622 1622 <url scheme: 'http', host: 'host', path: 'a?b#c'>
1623 1623 """
1624 1624
1625 1625 _safechars = "!~*'()+"
1626 1626 _safepchars = "/!~*'()+:"
1627 1627 _matchscheme = re.compile(r'^[a-zA-Z0-9+.\-]+:').match
1628 1628
1629 1629 def __init__(self, path, parsequery=True, parsefragment=True):
1630 1630 # We slowly chomp away at path until we have only the path left
1631 1631 self.scheme = self.user = self.passwd = self.host = None
1632 1632 self.port = self.path = self.query = self.fragment = None
1633 1633 self._localpath = True
1634 1634 self._hostport = ''
1635 1635 self._origpath = path
1636 1636
1637 1637 if parsefragment and '#' in path:
1638 1638 path, self.fragment = path.split('#', 1)
1639 1639 if not path:
1640 1640 path = None
1641 1641
1642 1642 # special case for Windows drive letters and UNC paths
1643 1643 if hasdriveletter(path) or path.startswith(r'\\'):
1644 1644 self.path = path
1645 1645 return
1646 1646
1647 1647 # For compatibility reasons, we can't handle bundle paths as
1648 1648 # normal URLS
1649 1649 if path.startswith('bundle:'):
1650 1650 self.scheme = 'bundle'
1651 1651 path = path[7:]
1652 1652 if path.startswith('//'):
1653 1653 path = path[2:]
1654 1654 self.path = path
1655 1655 return
1656 1656
1657 1657 if self._matchscheme(path):
1658 1658 parts = path.split(':', 1)
1659 1659 if parts[0]:
1660 1660 self.scheme, path = parts
1661 1661 self._localpath = False
1662 1662
1663 1663 if not path:
1664 1664 path = None
1665 1665 if self._localpath:
1666 1666 self.path = ''
1667 1667 return
1668 1668 else:
1669 1669 if self._localpath:
1670 1670 self.path = path
1671 1671 return
1672 1672
1673 1673 if parsequery and '?' in path:
1674 1674 path, self.query = path.split('?', 1)
1675 1675 if not path:
1676 1676 path = None
1677 1677 if not self.query:
1678 1678 self.query = None
1679 1679
1680 1680 # // is required to specify a host/authority
1681 1681 if path and path.startswith('//'):
1682 1682 parts = path[2:].split('/', 1)
1683 1683 if len(parts) > 1:
1684 1684 self.host, path = parts
1685 1685 path = path
1686 1686 else:
1687 1687 self.host = parts[0]
1688 1688 path = None
1689 1689 if not self.host:
1690 1690 self.host = None
1691 1691 # path of file:///d is /d
1692 1692 # path of file:///d:/ is d:/, not /d:/
1693 1693 if path and not hasdriveletter(path):
1694 1694 path = '/' + path
1695 1695
1696 1696 if self.host and '@' in self.host:
1697 1697 self.user, self.host = self.host.rsplit('@', 1)
1698 1698 if ':' in self.user:
1699 1699 self.user, self.passwd = self.user.split(':', 1)
1700 1700 if not self.host:
1701 1701 self.host = None
1702 1702
1703 1703 # Don't split on colons in IPv6 addresses without ports
1704 1704 if (self.host and ':' in self.host and
1705 1705 not (self.host.startswith('[') and self.host.endswith(']'))):
1706 1706 self._hostport = self.host
1707 1707 self.host, self.port = self.host.rsplit(':', 1)
1708 1708 if not self.host:
1709 1709 self.host = None
1710 1710
1711 1711 if (self.host and self.scheme == 'file' and
1712 1712 self.host not in ('localhost', '127.0.0.1', '[::1]')):
1713 1713 raise Abort(_('file:// URLs can only refer to localhost'))
1714 1714
1715 1715 self.path = path
1716 1716
1717 1717 # leave the query string escaped
1718 1718 for a in ('user', 'passwd', 'host', 'port',
1719 1719 'path', 'fragment'):
1720 1720 v = getattr(self, a)
1721 1721 if v is not None:
1722 1722 setattr(self, a, _urlunquote(v))
1723 1723
1724 1724 def __repr__(self):
1725 1725 attrs = []
1726 1726 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
1727 1727 'query', 'fragment'):
1728 1728 v = getattr(self, a)
1729 1729 if v is not None:
1730 1730 attrs.append('%s: %r' % (a, v))
1731 1731 return '<url %s>' % ', '.join(attrs)
1732 1732
1733 1733 def __str__(self):
1734 1734 r"""Join the URL's components back into a URL string.
1735 1735
1736 1736 Examples:
1737 1737
1738 1738 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
1739 1739 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
1740 1740 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
1741 1741 'http://user:pw@host:80/?foo=bar&baz=42'
1742 1742 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
1743 1743 'http://user:pw@host:80/?foo=bar%3dbaz'
1744 1744 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
1745 1745 'ssh://user:pw@[::1]:2200//home/joe#'
1746 1746 >>> str(url('http://localhost:80//'))
1747 1747 'http://localhost:80//'
1748 1748 >>> str(url('http://localhost:80/'))
1749 1749 'http://localhost:80/'
1750 1750 >>> str(url('http://localhost:80'))
1751 1751 'http://localhost:80/'
1752 1752 >>> str(url('bundle:foo'))
1753 1753 'bundle:foo'
1754 1754 >>> str(url('bundle://../foo'))
1755 1755 'bundle:../foo'
1756 1756 >>> str(url('path'))
1757 1757 'path'
1758 1758 >>> str(url('file:///tmp/foo/bar'))
1759 1759 'file:///tmp/foo/bar'
1760 1760 >>> str(url('file:///c:/tmp/foo/bar'))
1761 1761 'file:///c:/tmp/foo/bar'
1762 1762 >>> print url(r'bundle:foo\bar')
1763 1763 bundle:foo\bar
1764 1764 """
1765 1765 if self._localpath:
1766 1766 s = self.path
1767 1767 if self.scheme == 'bundle':
1768 1768 s = 'bundle:' + s
1769 1769 if self.fragment:
1770 1770 s += '#' + self.fragment
1771 1771 return s
1772 1772
1773 1773 s = self.scheme + ':'
1774 1774 if self.user or self.passwd or self.host:
1775 1775 s += '//'
1776 1776 elif self.scheme and (not self.path or self.path.startswith('/')
1777 1777 or hasdriveletter(self.path)):
1778 1778 s += '//'
1779 1779 if hasdriveletter(self.path):
1780 1780 s += '/'
1781 1781 if self.user:
1782 1782 s += urllib.quote(self.user, safe=self._safechars)
1783 1783 if self.passwd:
1784 1784 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
1785 1785 if self.user or self.passwd:
1786 1786 s += '@'
1787 1787 if self.host:
1788 1788 if not (self.host.startswith('[') and self.host.endswith(']')):
1789 1789 s += urllib.quote(self.host)
1790 1790 else:
1791 1791 s += self.host
1792 1792 if self.port:
1793 1793 s += ':' + urllib.quote(self.port)
1794 1794 if self.host:
1795 1795 s += '/'
1796 1796 if self.path:
1797 1797 # TODO: similar to the query string, we should not unescape the
1798 1798 # path when we store it, the path might contain '%2f' = '/',
1799 1799 # which we should *not* escape.
1800 1800 s += urllib.quote(self.path, safe=self._safepchars)
1801 1801 if self.query:
1802 1802 # we store the query in escaped form.
1803 1803 s += '?' + self.query
1804 1804 if self.fragment is not None:
1805 1805 s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
1806 1806 return s
1807 1807
1808 1808 def authinfo(self):
1809 1809 user, passwd = self.user, self.passwd
1810 1810 try:
1811 1811 self.user, self.passwd = None, None
1812 1812 s = str(self)
1813 1813 finally:
1814 1814 self.user, self.passwd = user, passwd
1815 1815 if not self.user:
1816 1816 return (s, None)
1817 1817 # authinfo[1] is passed to urllib2 password manager, and its
1818 1818 # URIs must not contain credentials. The host is passed in the
1819 1819 # URIs list because Python < 2.4.3 uses only that to search for
1820 1820 # a password.
1821 1821 return (s, (None, (s, self.host),
1822 1822 self.user, self.passwd or ''))
1823 1823
1824 1824 def isabs(self):
1825 1825 if self.scheme and self.scheme != 'file':
1826 1826 return True # remote URL
1827 1827 if hasdriveletter(self.path):
1828 1828 return True # absolute for our purposes - can't be joined()
1829 1829 if self.path.startswith(r'\\'):
1830 1830 return True # Windows UNC path
1831 1831 if self.path.startswith('/'):
1832 1832 return True # POSIX-style
1833 1833 return False
1834 1834
1835 1835 def localpath(self):
1836 1836 if self.scheme == 'file' or self.scheme == 'bundle':
1837 1837 path = self.path or '/'
1838 1838 # For Windows, we need to promote hosts containing drive
1839 1839 # letters to paths with drive letters.
1840 1840 if hasdriveletter(self._hostport):
1841 1841 path = self._hostport + '/' + self.path
1842 1842 elif (self.host is not None and self.path
1843 1843 and not hasdriveletter(path)):
1844 1844 path = '/' + path
1845 1845 return path
1846 1846 return self._origpath
1847 1847
1848 1848 def hasscheme(path):
1849 1849 return bool(url(path).scheme)
1850 1850
1851 1851 def hasdriveletter(path):
1852 1852 return path and path[1:2] == ':' and path[0:1].isalpha()
1853 1853
1854 1854 def urllocalpath(path):
1855 1855 return url(path, parsequery=False, parsefragment=False).localpath()
1856 1856
1857 1857 def hidepassword(u):
1858 1858 '''hide user credential in a url string'''
1859 1859 u = url(u)
1860 1860 if u.passwd:
1861 1861 u.passwd = '***'
1862 1862 return str(u)
1863 1863
1864 1864 def removeauth(u):
1865 1865 '''remove all authentication information from a url string'''
1866 1866 u = url(u)
1867 1867 u.user = u.passwd = None
1868 1868 return str(u)
1869 1869
1870 1870 def isatty(fd):
1871 1871 try:
1872 1872 return fd.isatty()
1873 1873 except AttributeError:
1874 1874 return False
1875
1876 timecount = unitcountfn(
1877 (1, 1e3, _('%.0f s')),
1878 (100, 1, _('%.1f s')),
1879 (10, 1, _('%.2f s')),
1880 (1, 1, _('%.3f s')),
1881 (100, 0.001, _('%.1f ms')),
1882 (10, 0.001, _('%.2f ms')),
1883 (1, 0.001, _('%.3f ms')),
1884 (100, 0.000001, _('%.1f us')),
1885 (10, 0.000001, _('%.2f us')),
1886 (1, 0.000001, _('%.3f us')),
1887 (100, 0.000000001, _('%.1f ns')),
1888 (10, 0.000000001, _('%.2f ns')),
1889 (1, 0.000000001, _('%.3f ns')),
1890 )
1891
1892 _timenesting = [0]
1893
1894 def timed(func):
1895 '''Report the execution time of a function call to stderr.
1896
1897 During development, use as a decorator when you need to measure
1898 the cost of a function, e.g. as follows:
1899
1900 @util.timed
1901 def foo(a, b, c):
1902 pass
1903 '''
1904
1905 def wrapper(*args, **kwargs):
1906 start = time.time()
1907 indent = 2
1908 _timenesting[0] += indent
1909 try:
1910 return func(*args, **kwargs)
1911 finally:
1912 elapsed = time.time() - start
1913 _timenesting[0] -= indent
1914 sys.stderr.write('%s%s: %s\n' %
1915 (' ' * _timenesting[0], func.__name__,
1916 timecount(elapsed)))
1917 return wrapper
General Comments 0
You need to be logged in to leave comments. Login now