##// END OF EJS Templates
ui: add configlist doctest to document a bit more of the whitespace behavior...
Augie Fackler -
r34958:58e7791e default
parent child Browse files
Show More
@@ -1,1836 +1,1839
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 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
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import getpass
14 14 import inspect
15 15 import os
16 16 import re
17 17 import signal
18 18 import socket
19 19 import subprocess
20 20 import sys
21 21 import tempfile
22 22 import traceback
23 23
24 24 from .i18n import _
25 25 from .node import hex
26 26
27 27 from . import (
28 28 color,
29 29 config,
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 formatter,
34 34 progress,
35 35 pycompat,
36 36 rcutil,
37 37 scmutil,
38 38 util,
39 39 )
40 40
41 41 urlreq = util.urlreq
42 42
43 43 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
44 44 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
45 45 if not c.isalnum())
46 46
47 47 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
48 48 tweakrc = """
49 49 [ui]
50 50 # The rollback command is dangerous. As a rule, don't use it.
51 51 rollback = False
52 52
53 53 [commands]
54 54 # Make `hg status` emit cwd-relative paths by default.
55 55 status.relative = yes
56 56 # Refuse to perform an `hg update` that would cause a file content merge
57 57 update.check = noconflict
58 58
59 59 [diff]
60 60 git = 1
61 61 """
62 62
63 63 samplehgrcs = {
64 64 'user':
65 65 b"""# example user config (see 'hg help config' for more info)
66 66 [ui]
67 67 # name and email, e.g.
68 68 # username = Jane Doe <jdoe@example.com>
69 69 username =
70 70
71 71 # We recommend enabling tweakdefaults to get slight improvements to
72 72 # the UI over time. Make sure to set HGPLAIN in the environment when
73 73 # writing scripts!
74 74 # tweakdefaults = True
75 75
76 76 # uncomment to disable color in command output
77 77 # (see 'hg help color' for details)
78 78 # color = never
79 79
80 80 # uncomment to disable command output pagination
81 81 # (see 'hg help pager' for details)
82 82 # paginate = never
83 83
84 84 [extensions]
85 85 # uncomment these lines to enable some popular extensions
86 86 # (see 'hg help extensions' for more info)
87 87 #
88 88 # churn =
89 89 """,
90 90
91 91 'cloned':
92 92 b"""# example repository config (see 'hg help config' for more info)
93 93 [paths]
94 94 default = %s
95 95
96 96 # path aliases to other clones of this repo in URLs or filesystem paths
97 97 # (see 'hg help config.paths' for more info)
98 98 #
99 99 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
100 100 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
101 101 # my-clone = /home/jdoe/jdoes-clone
102 102
103 103 [ui]
104 104 # name and email (local to this repository, optional), e.g.
105 105 # username = Jane Doe <jdoe@example.com>
106 106 """,
107 107
108 108 'local':
109 109 b"""# example repository config (see 'hg help config' for more info)
110 110 [paths]
111 111 # path aliases to other clones of this repo in URLs or filesystem paths
112 112 # (see 'hg help config.paths' for more info)
113 113 #
114 114 # default = http://example.com/hg/example-repo
115 115 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
116 116 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
117 117 # my-clone = /home/jdoe/jdoes-clone
118 118
119 119 [ui]
120 120 # name and email (local to this repository, optional), e.g.
121 121 # username = Jane Doe <jdoe@example.com>
122 122 """,
123 123
124 124 'global':
125 125 b"""# example system-wide hg config (see 'hg help config' for more info)
126 126
127 127 [ui]
128 128 # uncomment to disable color in command output
129 129 # (see 'hg help color' for details)
130 130 # color = never
131 131
132 132 # uncomment to disable command output pagination
133 133 # (see 'hg help pager' for details)
134 134 # paginate = never
135 135
136 136 [extensions]
137 137 # uncomment these lines to enable some popular extensions
138 138 # (see 'hg help extensions' for more info)
139 139 #
140 140 # blackbox =
141 141 # churn =
142 142 """,
143 143 }
144 144
145 145 def _maybestrurl(maybebytes):
146 146 if maybebytes is None:
147 147 return None
148 148 return pycompat.strurl(maybebytes)
149 149
150 150 def _maybebytesurl(maybestr):
151 151 if maybestr is None:
152 152 return None
153 153 return pycompat.bytesurl(maybestr)
154 154
155 155 class httppasswordmgrdbproxy(object):
156 156 """Delays loading urllib2 until it's needed."""
157 157 def __init__(self):
158 158 self._mgr = None
159 159
160 160 def _get_mgr(self):
161 161 if self._mgr is None:
162 162 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
163 163 return self._mgr
164 164
165 165 def add_password(self, realm, uris, user, passwd):
166 166 if isinstance(uris, tuple):
167 167 uris = tuple(_maybestrurl(u) for u in uris)
168 168 else:
169 169 uris = _maybestrurl(uris)
170 170 return self._get_mgr().add_password(
171 171 _maybestrurl(realm), uris,
172 172 _maybestrurl(user), _maybestrurl(passwd))
173 173
174 174 def find_user_password(self, realm, uri):
175 175 return tuple(_maybebytesurl(v) for v in
176 176 self._get_mgr().find_user_password(_maybestrurl(realm),
177 177 _maybestrurl(uri)))
178 178
179 179 def _catchterm(*args):
180 180 raise error.SignalInterrupt
181 181
182 182 # unique object used to detect no default value has been provided when
183 183 # retrieving configuration value.
184 184 _unset = object()
185 185
186 186 # _reqexithandlers: callbacks run at the end of a request
187 187 _reqexithandlers = []
188 188
189 189 class ui(object):
190 190 def __init__(self, src=None):
191 191 """Create a fresh new ui object if no src given
192 192
193 193 Use uimod.ui.load() to create a ui which knows global and user configs.
194 194 In most cases, you should use ui.copy() to create a copy of an existing
195 195 ui object.
196 196 """
197 197 # _buffers: used for temporary capture of output
198 198 self._buffers = []
199 199 # 3-tuple describing how each buffer in the stack behaves.
200 200 # Values are (capture stderr, capture subprocesses, apply labels).
201 201 self._bufferstates = []
202 202 # When a buffer is active, defines whether we are expanding labels.
203 203 # This exists to prevent an extra list lookup.
204 204 self._bufferapplylabels = None
205 205 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
206 206 self._reportuntrusted = True
207 207 self._knownconfig = configitems.coreitems
208 208 self._ocfg = config.config() # overlay
209 209 self._tcfg = config.config() # trusted
210 210 self._ucfg = config.config() # untrusted
211 211 self._trustusers = set()
212 212 self._trustgroups = set()
213 213 self.callhooks = True
214 214 # Insecure server connections requested.
215 215 self.insecureconnections = False
216 216 # Blocked time
217 217 self.logblockedtimes = False
218 218 # color mode: see mercurial/color.py for possible value
219 219 self._colormode = None
220 220 self._terminfoparams = {}
221 221 self._styles = {}
222 222
223 223 if src:
224 224 self.fout = src.fout
225 225 self.ferr = src.ferr
226 226 self.fin = src.fin
227 227 self.pageractive = src.pageractive
228 228 self._disablepager = src._disablepager
229 229 self._tweaked = src._tweaked
230 230
231 231 self._tcfg = src._tcfg.copy()
232 232 self._ucfg = src._ucfg.copy()
233 233 self._ocfg = src._ocfg.copy()
234 234 self._trustusers = src._trustusers.copy()
235 235 self._trustgroups = src._trustgroups.copy()
236 236 self.environ = src.environ
237 237 self.callhooks = src.callhooks
238 238 self.insecureconnections = src.insecureconnections
239 239 self._colormode = src._colormode
240 240 self._terminfoparams = src._terminfoparams.copy()
241 241 self._styles = src._styles.copy()
242 242
243 243 self.fixconfig()
244 244
245 245 self.httppasswordmgrdb = src.httppasswordmgrdb
246 246 self._blockedtimes = src._blockedtimes
247 247 else:
248 248 self.fout = util.stdout
249 249 self.ferr = util.stderr
250 250 self.fin = util.stdin
251 251 self.pageractive = False
252 252 self._disablepager = False
253 253 self._tweaked = False
254 254
255 255 # shared read-only environment
256 256 self.environ = encoding.environ
257 257
258 258 self.httppasswordmgrdb = httppasswordmgrdbproxy()
259 259 self._blockedtimes = collections.defaultdict(int)
260 260
261 261 allowed = self.configlist('experimental', 'exportableenviron')
262 262 if '*' in allowed:
263 263 self._exportableenviron = self.environ
264 264 else:
265 265 self._exportableenviron = {}
266 266 for k in allowed:
267 267 if k in self.environ:
268 268 self._exportableenviron[k] = self.environ[k]
269 269
270 270 @classmethod
271 271 def load(cls):
272 272 """Create a ui and load global and user configs"""
273 273 u = cls()
274 274 # we always trust global config files and environment variables
275 275 for t, f in rcutil.rccomponents():
276 276 if t == 'path':
277 277 u.readconfig(f, trust=True)
278 278 elif t == 'items':
279 279 sections = set()
280 280 for section, name, value, source in f:
281 281 # do not set u._ocfg
282 282 # XXX clean this up once immutable config object is a thing
283 283 u._tcfg.set(section, name, value, source)
284 284 u._ucfg.set(section, name, value, source)
285 285 sections.add(section)
286 286 for section in sections:
287 287 u.fixconfig(section=section)
288 288 else:
289 289 raise error.ProgrammingError('unknown rctype: %s' % t)
290 290 u._maybetweakdefaults()
291 291 return u
292 292
293 293 def _maybetweakdefaults(self):
294 294 if not self.configbool('ui', 'tweakdefaults'):
295 295 return
296 296 if self._tweaked or self.plain('tweakdefaults'):
297 297 return
298 298
299 299 # Note: it is SUPER IMPORTANT that you set self._tweaked to
300 300 # True *before* any calls to setconfig(), otherwise you'll get
301 301 # infinite recursion between setconfig and this method.
302 302 #
303 303 # TODO: We should extract an inner method in setconfig() to
304 304 # avoid this weirdness.
305 305 self._tweaked = True
306 306 tmpcfg = config.config()
307 307 tmpcfg.parse('<tweakdefaults>', tweakrc)
308 308 for section in tmpcfg:
309 309 for name, value in tmpcfg.items(section):
310 310 if not self.hasconfig(section, name):
311 311 self.setconfig(section, name, value, "<tweakdefaults>")
312 312
313 313 def copy(self):
314 314 return self.__class__(self)
315 315
316 316 def resetstate(self):
317 317 """Clear internal state that shouldn't persist across commands"""
318 318 if self._progbar:
319 319 self._progbar.resetstate() # reset last-print time of progress bar
320 320 self.httppasswordmgrdb = httppasswordmgrdbproxy()
321 321
322 322 @contextlib.contextmanager
323 323 def timeblockedsection(self, key):
324 324 # this is open-coded below - search for timeblockedsection to find them
325 325 starttime = util.timer()
326 326 try:
327 327 yield
328 328 finally:
329 329 self._blockedtimes[key + '_blocked'] += \
330 330 (util.timer() - starttime) * 1000
331 331
332 332 def formatter(self, topic, opts):
333 333 return formatter.formatter(self, self, topic, opts)
334 334
335 335 def _trusted(self, fp, f):
336 336 st = util.fstat(fp)
337 337 if util.isowner(st):
338 338 return True
339 339
340 340 tusers, tgroups = self._trustusers, self._trustgroups
341 341 if '*' in tusers or '*' in tgroups:
342 342 return True
343 343
344 344 user = util.username(st.st_uid)
345 345 group = util.groupname(st.st_gid)
346 346 if user in tusers or group in tgroups or user == util.username():
347 347 return True
348 348
349 349 if self._reportuntrusted:
350 350 self.warn(_('not trusting file %s from untrusted '
351 351 'user %s, group %s\n') % (f, user, group))
352 352 return False
353 353
354 354 def readconfig(self, filename, root=None, trust=False,
355 355 sections=None, remap=None):
356 356 try:
357 357 fp = open(filename, u'rb')
358 358 except IOError:
359 359 if not sections: # ignore unless we were looking for something
360 360 return
361 361 raise
362 362
363 363 cfg = config.config()
364 364 trusted = sections or trust or self._trusted(fp, filename)
365 365
366 366 try:
367 367 cfg.read(filename, fp, sections=sections, remap=remap)
368 368 fp.close()
369 369 except error.ConfigError as inst:
370 370 if trusted:
371 371 raise
372 372 self.warn(_("ignored: %s\n") % str(inst))
373 373
374 374 if self.plain():
375 375 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
376 376 'logtemplate', 'statuscopies', 'style',
377 377 'traceback', 'verbose'):
378 378 if k in cfg['ui']:
379 379 del cfg['ui'][k]
380 380 for k, v in cfg.items('defaults'):
381 381 del cfg['defaults'][k]
382 382 for k, v in cfg.items('commands'):
383 383 del cfg['commands'][k]
384 384 # Don't remove aliases from the configuration if in the exceptionlist
385 385 if self.plain('alias'):
386 386 for k, v in cfg.items('alias'):
387 387 del cfg['alias'][k]
388 388 if self.plain('revsetalias'):
389 389 for k, v in cfg.items('revsetalias'):
390 390 del cfg['revsetalias'][k]
391 391 if self.plain('templatealias'):
392 392 for k, v in cfg.items('templatealias'):
393 393 del cfg['templatealias'][k]
394 394
395 395 if trusted:
396 396 self._tcfg.update(cfg)
397 397 self._tcfg.update(self._ocfg)
398 398 self._ucfg.update(cfg)
399 399 self._ucfg.update(self._ocfg)
400 400
401 401 if root is None:
402 402 root = os.path.expanduser('~')
403 403 self.fixconfig(root=root)
404 404
405 405 def fixconfig(self, root=None, section=None):
406 406 if section in (None, 'paths'):
407 407 # expand vars and ~
408 408 # translate paths relative to root (or home) into absolute paths
409 409 root = root or pycompat.getcwd()
410 410 for c in self._tcfg, self._ucfg, self._ocfg:
411 411 for n, p in c.items('paths'):
412 412 # Ignore sub-options.
413 413 if ':' in n:
414 414 continue
415 415 if not p:
416 416 continue
417 417 if '%%' in p:
418 418 s = self.configsource('paths', n) or 'none'
419 419 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
420 420 % (n, p, s))
421 421 p = p.replace('%%', '%')
422 422 p = util.expandpath(p)
423 423 if not util.hasscheme(p) and not os.path.isabs(p):
424 424 p = os.path.normpath(os.path.join(root, p))
425 425 c.set("paths", n, p)
426 426
427 427 if section in (None, 'ui'):
428 428 # update ui options
429 429 self.debugflag = self.configbool('ui', 'debug')
430 430 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
431 431 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
432 432 if self.verbose and self.quiet:
433 433 self.quiet = self.verbose = False
434 434 self._reportuntrusted = self.debugflag or self.configbool("ui",
435 435 "report_untrusted")
436 436 self.tracebackflag = self.configbool('ui', 'traceback')
437 437 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
438 438
439 439 if section in (None, 'trusted'):
440 440 # update trust information
441 441 self._trustusers.update(self.configlist('trusted', 'users'))
442 442 self._trustgroups.update(self.configlist('trusted', 'groups'))
443 443
444 444 def backupconfig(self, section, item):
445 445 return (self._ocfg.backup(section, item),
446 446 self._tcfg.backup(section, item),
447 447 self._ucfg.backup(section, item),)
448 448 def restoreconfig(self, data):
449 449 self._ocfg.restore(data[0])
450 450 self._tcfg.restore(data[1])
451 451 self._ucfg.restore(data[2])
452 452
453 453 def setconfig(self, section, name, value, source=''):
454 454 for cfg in (self._ocfg, self._tcfg, self._ucfg):
455 455 cfg.set(section, name, value, source)
456 456 self.fixconfig(section=section)
457 457 self._maybetweakdefaults()
458 458
459 459 def _data(self, untrusted):
460 460 return untrusted and self._ucfg or self._tcfg
461 461
462 462 def configsource(self, section, name, untrusted=False):
463 463 return self._data(untrusted).source(section, name)
464 464
465 465 def config(self, section, name, default=_unset, untrusted=False):
466 466 """return the plain string version of a config"""
467 467 value = self._config(section, name, default=default,
468 468 untrusted=untrusted)
469 469 if value is _unset:
470 470 return None
471 471 return value
472 472
473 473 def _config(self, section, name, default=_unset, untrusted=False):
474 474 value = itemdefault = default
475 475 item = self._knownconfig.get(section, {}).get(name)
476 476 alternates = [(section, name)]
477 477
478 478 if item is not None:
479 479 alternates.extend(item.alias)
480 480 if callable(item.default):
481 481 itemdefault = item.default()
482 482 else:
483 483 itemdefault = item.default
484 484 else:
485 485 msg = ("accessing unregistered config item: '%s.%s'")
486 486 msg %= (section, name)
487 487 self.develwarn(msg, 2, 'warn-config-unknown')
488 488
489 489 if default is _unset:
490 490 if item is None:
491 491 value = default
492 492 elif item.default is configitems.dynamicdefault:
493 493 value = None
494 494 msg = "config item requires an explicit default value: '%s.%s'"
495 495 msg %= (section, name)
496 496 self.develwarn(msg, 2, 'warn-config-default')
497 497 else:
498 498 value = itemdefault
499 499 elif (item is not None
500 500 and item.default is not configitems.dynamicdefault
501 501 and default != itemdefault):
502 502 msg = ("specifying a mismatched default value for a registered "
503 503 "config item: '%s.%s' '%s'")
504 504 msg %= (section, name, default)
505 505 self.develwarn(msg, 2, 'warn-config-default')
506 506
507 507 for s, n in alternates:
508 508 candidate = self._data(untrusted).get(s, n, None)
509 509 if candidate is not None:
510 510 value = candidate
511 511 section = s
512 512 name = n
513 513 break
514 514
515 515 if self.debugflag and not untrusted and self._reportuntrusted:
516 516 for s, n in alternates:
517 517 uvalue = self._ucfg.get(s, n)
518 518 if uvalue is not None and uvalue != value:
519 519 self.debug("ignoring untrusted configuration option "
520 520 "%s.%s = %s\n" % (s, n, uvalue))
521 521 return value
522 522
523 523 def configsuboptions(self, section, name, default=_unset, untrusted=False):
524 524 """Get a config option and all sub-options.
525 525
526 526 Some config options have sub-options that are declared with the
527 527 format "key:opt = value". This method is used to return the main
528 528 option and all its declared sub-options.
529 529
530 530 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
531 531 is a dict of defined sub-options where keys and values are strings.
532 532 """
533 533 main = self.config(section, name, default, untrusted=untrusted)
534 534 data = self._data(untrusted)
535 535 sub = {}
536 536 prefix = '%s:' % name
537 537 for k, v in data.items(section):
538 538 if k.startswith(prefix):
539 539 sub[k[len(prefix):]] = v
540 540
541 541 if self.debugflag and not untrusted and self._reportuntrusted:
542 542 for k, v in sub.items():
543 543 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
544 544 if uvalue is not None and uvalue != v:
545 545 self.debug('ignoring untrusted configuration option '
546 546 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
547 547
548 548 return main, sub
549 549
550 550 def configpath(self, section, name, default=_unset, untrusted=False):
551 551 'get a path config item, expanded relative to repo root or config file'
552 552 v = self.config(section, name, default, untrusted)
553 553 if v is None:
554 554 return None
555 555 if not os.path.isabs(v) or "://" not in v:
556 556 src = self.configsource(section, name, untrusted)
557 557 if ':' in src:
558 558 base = os.path.dirname(src.rsplit(':')[0])
559 559 v = os.path.join(base, os.path.expanduser(v))
560 560 return v
561 561
562 562 def configbool(self, section, name, default=_unset, untrusted=False):
563 563 """parse a configuration element as a boolean
564 564
565 565 >>> u = ui(); s = b'foo'
566 566 >>> u.setconfig(s, b'true', b'yes')
567 567 >>> u.configbool(s, b'true')
568 568 True
569 569 >>> u.setconfig(s, b'false', b'no')
570 570 >>> u.configbool(s, b'false')
571 571 False
572 572 >>> u.configbool(s, b'unknown')
573 573 False
574 574 >>> u.configbool(s, b'unknown', True)
575 575 True
576 576 >>> u.setconfig(s, b'invalid', b'somevalue')
577 577 >>> u.configbool(s, b'invalid')
578 578 Traceback (most recent call last):
579 579 ...
580 580 ConfigError: foo.invalid is not a boolean ('somevalue')
581 581 """
582 582
583 583 v = self._config(section, name, default, untrusted=untrusted)
584 584 if v is None:
585 585 return v
586 586 if v is _unset:
587 587 if default is _unset:
588 588 return False
589 589 return default
590 590 if isinstance(v, bool):
591 591 return v
592 592 b = util.parsebool(v)
593 593 if b is None:
594 594 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
595 595 % (section, name, v))
596 596 return b
597 597
598 598 def configwith(self, convert, section, name, default=_unset,
599 599 desc=None, untrusted=False):
600 600 """parse a configuration element with a conversion function
601 601
602 602 >>> u = ui(); s = b'foo'
603 603 >>> u.setconfig(s, b'float1', b'42')
604 604 >>> u.configwith(float, s, b'float1')
605 605 42.0
606 606 >>> u.setconfig(s, b'float2', b'-4.25')
607 607 >>> u.configwith(float, s, b'float2')
608 608 -4.25
609 609 >>> u.configwith(float, s, b'unknown', 7)
610 610 7.0
611 611 >>> u.setconfig(s, b'invalid', b'somevalue')
612 612 >>> u.configwith(float, s, b'invalid')
613 613 Traceback (most recent call last):
614 614 ...
615 615 ConfigError: foo.invalid is not a valid float ('somevalue')
616 616 >>> u.configwith(float, s, b'invalid', desc=b'womble')
617 617 Traceback (most recent call last):
618 618 ...
619 619 ConfigError: foo.invalid is not a valid womble ('somevalue')
620 620 """
621 621
622 622 v = self.config(section, name, default, untrusted)
623 623 if v is None:
624 624 return v # do not attempt to convert None
625 625 try:
626 626 return convert(v)
627 627 except (ValueError, error.ParseError):
628 628 if desc is None:
629 629 desc = pycompat.sysbytes(convert.__name__)
630 630 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
631 631 % (section, name, desc, v))
632 632
633 633 def configint(self, section, name, default=_unset, untrusted=False):
634 634 """parse a configuration element as an integer
635 635
636 636 >>> u = ui(); s = b'foo'
637 637 >>> u.setconfig(s, b'int1', b'42')
638 638 >>> u.configint(s, b'int1')
639 639 42
640 640 >>> u.setconfig(s, b'int2', b'-42')
641 641 >>> u.configint(s, b'int2')
642 642 -42
643 643 >>> u.configint(s, b'unknown', 7)
644 644 7
645 645 >>> u.setconfig(s, b'invalid', b'somevalue')
646 646 >>> u.configint(s, b'invalid')
647 647 Traceback (most recent call last):
648 648 ...
649 649 ConfigError: foo.invalid is not a valid integer ('somevalue')
650 650 """
651 651
652 652 return self.configwith(int, section, name, default, 'integer',
653 653 untrusted)
654 654
655 655 def configbytes(self, section, name, default=_unset, untrusted=False):
656 656 """parse a configuration element as a quantity in bytes
657 657
658 658 Units can be specified as b (bytes), k or kb (kilobytes), m or
659 659 mb (megabytes), g or gb (gigabytes).
660 660
661 661 >>> u = ui(); s = b'foo'
662 662 >>> u.setconfig(s, b'val1', b'42')
663 663 >>> u.configbytes(s, b'val1')
664 664 42
665 665 >>> u.setconfig(s, b'val2', b'42.5 kb')
666 666 >>> u.configbytes(s, b'val2')
667 667 43520
668 668 >>> u.configbytes(s, b'unknown', b'7 MB')
669 669 7340032
670 670 >>> u.setconfig(s, b'invalid', b'somevalue')
671 671 >>> u.configbytes(s, b'invalid')
672 672 Traceback (most recent call last):
673 673 ...
674 674 ConfigError: foo.invalid is not a byte quantity ('somevalue')
675 675 """
676 676
677 677 value = self._config(section, name, default, untrusted)
678 678 if value is _unset:
679 679 if default is _unset:
680 680 default = 0
681 681 value = default
682 682 if not isinstance(value, bytes):
683 683 return value
684 684 try:
685 685 return util.sizetoint(value)
686 686 except error.ParseError:
687 687 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
688 688 % (section, name, value))
689 689
690 690 def configlist(self, section, name, default=_unset, untrusted=False):
691 691 """parse a configuration element as a list of comma/space separated
692 692 strings
693 693
694 694 >>> u = ui(); s = b'foo'
695 695 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
696 696 >>> u.configlist(s, b'list1')
697 697 ['this', 'is', 'a small', 'test']
698 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
699 >>> u.configlist(s, b'list2')
700 ['this', 'is', 'a small', 'test']
698 701 """
699 702 # default is not always a list
700 703 v = self.configwith(config.parselist, section, name, default,
701 704 'list', untrusted)
702 705 if isinstance(v, bytes):
703 706 return config.parselist(v)
704 707 elif v is None:
705 708 return []
706 709 return v
707 710
708 711 def configdate(self, section, name, default=_unset, untrusted=False):
709 712 """parse a configuration element as a tuple of ints
710 713
711 714 >>> u = ui(); s = b'foo'
712 715 >>> u.setconfig(s, b'date', b'0 0')
713 716 >>> u.configdate(s, b'date')
714 717 (0, 0)
715 718 """
716 719 if self.config(section, name, default, untrusted):
717 720 return self.configwith(util.parsedate, section, name, default,
718 721 'date', untrusted)
719 722 if default is _unset:
720 723 return None
721 724 return default
722 725
723 726 def hasconfig(self, section, name, untrusted=False):
724 727 return self._data(untrusted).hasitem(section, name)
725 728
726 729 def has_section(self, section, untrusted=False):
727 730 '''tell whether section exists in config.'''
728 731 return section in self._data(untrusted)
729 732
730 733 def configitems(self, section, untrusted=False, ignoresub=False):
731 734 items = self._data(untrusted).items(section)
732 735 if ignoresub:
733 736 newitems = {}
734 737 for k, v in items:
735 738 if ':' not in k:
736 739 newitems[k] = v
737 740 items = newitems.items()
738 741 if self.debugflag and not untrusted and self._reportuntrusted:
739 742 for k, v in self._ucfg.items(section):
740 743 if self._tcfg.get(section, k) != v:
741 744 self.debug("ignoring untrusted configuration option "
742 745 "%s.%s = %s\n" % (section, k, v))
743 746 return items
744 747
745 748 def walkconfig(self, untrusted=False):
746 749 cfg = self._data(untrusted)
747 750 for section in cfg.sections():
748 751 for name, value in self.configitems(section, untrusted):
749 752 yield section, name, value
750 753
751 754 def plain(self, feature=None):
752 755 '''is plain mode active?
753 756
754 757 Plain mode means that all configuration variables which affect
755 758 the behavior and output of Mercurial should be
756 759 ignored. Additionally, the output should be stable,
757 760 reproducible and suitable for use in scripts or applications.
758 761
759 762 The only way to trigger plain mode is by setting either the
760 763 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
761 764
762 765 The return value can either be
763 766 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
764 767 - True otherwise
765 768 '''
766 769 if ('HGPLAIN' not in encoding.environ and
767 770 'HGPLAINEXCEPT' not in encoding.environ):
768 771 return False
769 772 exceptions = encoding.environ.get('HGPLAINEXCEPT',
770 773 '').strip().split(',')
771 774 if feature and exceptions:
772 775 return feature not in exceptions
773 776 return True
774 777
775 778 def username(self, acceptempty=False):
776 779 """Return default username to be used in commits.
777 780
778 781 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
779 782 and stop searching if one of these is set.
780 783 If not found and acceptempty is True, returns None.
781 784 If not found and ui.askusername is True, ask the user, else use
782 785 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
783 786 If no username could be found, raise an Abort error.
784 787 """
785 788 user = encoding.environ.get("HGUSER")
786 789 if user is None:
787 790 user = self.config("ui", "username")
788 791 if user is not None:
789 792 user = os.path.expandvars(user)
790 793 if user is None:
791 794 user = encoding.environ.get("EMAIL")
792 795 if user is None and acceptempty:
793 796 return user
794 797 if user is None and self.configbool("ui", "askusername"):
795 798 user = self.prompt(_("enter a commit username:"), default=None)
796 799 if user is None and not self.interactive():
797 800 try:
798 801 user = '%s@%s' % (util.getuser(), socket.getfqdn())
799 802 self.warn(_("no username found, using '%s' instead\n") % user)
800 803 except KeyError:
801 804 pass
802 805 if not user:
803 806 raise error.Abort(_('no username supplied'),
804 807 hint=_("use 'hg config --edit' "
805 808 'to set your username'))
806 809 if "\n" in user:
807 810 raise error.Abort(_("username %s contains a newline\n")
808 811 % repr(user))
809 812 return user
810 813
811 814 def shortuser(self, user):
812 815 """Return a short representation of a user name or email address."""
813 816 if not self.verbose:
814 817 user = util.shortuser(user)
815 818 return user
816 819
817 820 def expandpath(self, loc, default=None):
818 821 """Return repository location relative to cwd or from [paths]"""
819 822 try:
820 823 p = self.paths.getpath(loc)
821 824 if p:
822 825 return p.rawloc
823 826 except error.RepoError:
824 827 pass
825 828
826 829 if default:
827 830 try:
828 831 p = self.paths.getpath(default)
829 832 if p:
830 833 return p.rawloc
831 834 except error.RepoError:
832 835 pass
833 836
834 837 return loc
835 838
836 839 @util.propertycache
837 840 def paths(self):
838 841 return paths(self)
839 842
840 843 def pushbuffer(self, error=False, subproc=False, labeled=False):
841 844 """install a buffer to capture standard output of the ui object
842 845
843 846 If error is True, the error output will be captured too.
844 847
845 848 If subproc is True, output from subprocesses (typically hooks) will be
846 849 captured too.
847 850
848 851 If labeled is True, any labels associated with buffered
849 852 output will be handled. By default, this has no effect
850 853 on the output returned, but extensions and GUI tools may
851 854 handle this argument and returned styled output. If output
852 855 is being buffered so it can be captured and parsed or
853 856 processed, labeled should not be set to True.
854 857 """
855 858 self._buffers.append([])
856 859 self._bufferstates.append((error, subproc, labeled))
857 860 self._bufferapplylabels = labeled
858 861
859 862 def popbuffer(self):
860 863 '''pop the last buffer and return the buffered output'''
861 864 self._bufferstates.pop()
862 865 if self._bufferstates:
863 866 self._bufferapplylabels = self._bufferstates[-1][2]
864 867 else:
865 868 self._bufferapplylabels = None
866 869
867 870 return "".join(self._buffers.pop())
868 871
869 872 def write(self, *args, **opts):
870 873 '''write args to output
871 874
872 875 By default, this method simply writes to the buffer or stdout.
873 876 Color mode can be set on the UI class to have the output decorated
874 877 with color modifier before being written to stdout.
875 878
876 879 The color used is controlled by an optional keyword argument, "label".
877 880 This should be a string containing label names separated by space.
878 881 Label names take the form of "topic.type". For example, ui.debug()
879 882 issues a label of "ui.debug".
880 883
881 884 When labeling output for a specific command, a label of
882 885 "cmdname.type" is recommended. For example, status issues
883 886 a label of "status.modified" for modified files.
884 887 '''
885 888 if self._buffers and not opts.get('prompt', False):
886 889 if self._bufferapplylabels:
887 890 label = opts.get('label', '')
888 891 self._buffers[-1].extend(self.label(a, label) for a in args)
889 892 else:
890 893 self._buffers[-1].extend(args)
891 894 elif self._colormode == 'win32':
892 895 # windows color printing is its own can of crab, defer to
893 896 # the color module and that is it.
894 897 color.win32print(self, self._write, *args, **opts)
895 898 else:
896 899 msgs = args
897 900 if self._colormode is not None:
898 901 label = opts.get('label', '')
899 902 msgs = [self.label(a, label) for a in args]
900 903 self._write(*msgs, **opts)
901 904
902 905 def _write(self, *msgs, **opts):
903 906 self._progclear()
904 907 # opencode timeblockedsection because this is a critical path
905 908 starttime = util.timer()
906 909 try:
907 910 for a in msgs:
908 911 self.fout.write(a)
909 912 except IOError as err:
910 913 raise error.StdioError(err)
911 914 finally:
912 915 self._blockedtimes['stdio_blocked'] += \
913 916 (util.timer() - starttime) * 1000
914 917
915 918 def write_err(self, *args, **opts):
916 919 self._progclear()
917 920 if self._bufferstates and self._bufferstates[-1][0]:
918 921 self.write(*args, **opts)
919 922 elif self._colormode == 'win32':
920 923 # windows color printing is its own can of crab, defer to
921 924 # the color module and that is it.
922 925 color.win32print(self, self._write_err, *args, **opts)
923 926 else:
924 927 msgs = args
925 928 if self._colormode is not None:
926 929 label = opts.get('label', '')
927 930 msgs = [self.label(a, label) for a in args]
928 931 self._write_err(*msgs, **opts)
929 932
930 933 def _write_err(self, *msgs, **opts):
931 934 try:
932 935 with self.timeblockedsection('stdio'):
933 936 if not getattr(self.fout, 'closed', False):
934 937 self.fout.flush()
935 938 for a in msgs:
936 939 self.ferr.write(a)
937 940 # stderr may be buffered under win32 when redirected to files,
938 941 # including stdout.
939 942 if not getattr(self.ferr, 'closed', False):
940 943 self.ferr.flush()
941 944 except IOError as inst:
942 945 if inst.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
943 946 raise error.StdioError(inst)
944 947
945 948 def flush(self):
946 949 # opencode timeblockedsection because this is a critical path
947 950 starttime = util.timer()
948 951 try:
949 952 try:
950 953 self.fout.flush()
951 954 except IOError as err:
952 955 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
953 956 raise error.StdioError(err)
954 957 finally:
955 958 try:
956 959 self.ferr.flush()
957 960 except IOError as err:
958 961 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
959 962 raise error.StdioError(err)
960 963 finally:
961 964 self._blockedtimes['stdio_blocked'] += \
962 965 (util.timer() - starttime) * 1000
963 966
964 967 def _isatty(self, fh):
965 968 if self.configbool('ui', 'nontty'):
966 969 return False
967 970 return util.isatty(fh)
968 971
969 972 def disablepager(self):
970 973 self._disablepager = True
971 974
972 975 def pager(self, command):
973 976 """Start a pager for subsequent command output.
974 977
975 978 Commands which produce a long stream of output should call
976 979 this function to activate the user's preferred pagination
977 980 mechanism (which may be no pager). Calling this function
978 981 precludes any future use of interactive functionality, such as
979 982 prompting the user or activating curses.
980 983
981 984 Args:
982 985 command: The full, non-aliased name of the command. That is, "log"
983 986 not "history, "summary" not "summ", etc.
984 987 """
985 988 if (self._disablepager
986 989 or self.pageractive):
987 990 # how pager should do is already determined
988 991 return
989 992
990 993 if not command.startswith('internal-always-') and (
991 994 # explicit --pager=on (= 'internal-always-' prefix) should
992 995 # take precedence over disabling factors below
993 996 command in self.configlist('pager', 'ignore')
994 997 or not self.configbool('ui', 'paginate')
995 998 or not self.configbool('pager', 'attend-' + command, True)
996 999 # TODO: if we want to allow HGPLAINEXCEPT=pager,
997 1000 # formatted() will need some adjustment.
998 1001 or not self.formatted()
999 1002 or self.plain()
1000 1003 or self._buffers
1001 1004 # TODO: expose debugger-enabled on the UI object
1002 1005 or '--debugger' in pycompat.sysargv):
1003 1006 # We only want to paginate if the ui appears to be
1004 1007 # interactive, the user didn't say HGPLAIN or
1005 1008 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1006 1009 return
1007 1010
1008 1011 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1009 1012 if not pagercmd:
1010 1013 return
1011 1014
1012 1015 pagerenv = {}
1013 1016 for name, value in rcutil.defaultpagerenv().items():
1014 1017 if name not in encoding.environ:
1015 1018 pagerenv[name] = value
1016 1019
1017 1020 self.debug('starting pager for command %r\n' % command)
1018 1021 self.flush()
1019 1022
1020 1023 wasformatted = self.formatted()
1021 1024 if util.safehasattr(signal, "SIGPIPE"):
1022 1025 signal.signal(signal.SIGPIPE, _catchterm)
1023 1026 if self._runpager(pagercmd, pagerenv):
1024 1027 self.pageractive = True
1025 1028 # Preserve the formatted-ness of the UI. This is important
1026 1029 # because we mess with stdout, which might confuse
1027 1030 # auto-detection of things being formatted.
1028 1031 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1029 1032 self.setconfig('ui', 'interactive', False, 'pager')
1030 1033
1031 1034 # If pagermode differs from color.mode, reconfigure color now that
1032 1035 # pageractive is set.
1033 1036 cm = self._colormode
1034 1037 if cm != self.config('color', 'pagermode', cm):
1035 1038 color.setup(self)
1036 1039 else:
1037 1040 # If the pager can't be spawned in dispatch when --pager=on is
1038 1041 # given, don't try again when the command runs, to avoid a duplicate
1039 1042 # warning about a missing pager command.
1040 1043 self.disablepager()
1041 1044
1042 1045 def _runpager(self, command, env=None):
1043 1046 """Actually start the pager and set up file descriptors.
1044 1047
1045 1048 This is separate in part so that extensions (like chg) can
1046 1049 override how a pager is invoked.
1047 1050 """
1048 1051 if command == 'cat':
1049 1052 # Save ourselves some work.
1050 1053 return False
1051 1054 # If the command doesn't contain any of these characters, we
1052 1055 # assume it's a binary and exec it directly. This means for
1053 1056 # simple pager command configurations, we can degrade
1054 1057 # gracefully and tell the user about their broken pager.
1055 1058 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1056 1059
1057 1060 if pycompat.iswindows and not shell:
1058 1061 # Window's built-in `more` cannot be invoked with shell=False, but
1059 1062 # its `more.com` can. Hide this implementation detail from the
1060 1063 # user so we can also get sane bad PAGER behavior. MSYS has
1061 1064 # `more.exe`, so do a cmd.exe style resolution of the executable to
1062 1065 # determine which one to use.
1063 1066 fullcmd = util.findexe(command)
1064 1067 if not fullcmd:
1065 1068 self.warn(_("missing pager command '%s', skipping pager\n")
1066 1069 % command)
1067 1070 return False
1068 1071
1069 1072 command = fullcmd
1070 1073
1071 1074 try:
1072 1075 pager = subprocess.Popen(
1073 1076 command, shell=shell, bufsize=-1,
1074 1077 close_fds=util.closefds, stdin=subprocess.PIPE,
1075 1078 stdout=util.stdout, stderr=util.stderr,
1076 1079 env=util.shellenviron(env))
1077 1080 except OSError as e:
1078 1081 if e.errno == errno.ENOENT and not shell:
1079 1082 self.warn(_("missing pager command '%s', skipping pager\n")
1080 1083 % command)
1081 1084 return False
1082 1085 raise
1083 1086
1084 1087 # back up original file descriptors
1085 1088 stdoutfd = os.dup(util.stdout.fileno())
1086 1089 stderrfd = os.dup(util.stderr.fileno())
1087 1090
1088 1091 os.dup2(pager.stdin.fileno(), util.stdout.fileno())
1089 1092 if self._isatty(util.stderr):
1090 1093 os.dup2(pager.stdin.fileno(), util.stderr.fileno())
1091 1094
1092 1095 @self.atexit
1093 1096 def killpager():
1094 1097 if util.safehasattr(signal, "SIGINT"):
1095 1098 signal.signal(signal.SIGINT, signal.SIG_IGN)
1096 1099 # restore original fds, closing pager.stdin copies in the process
1097 1100 os.dup2(stdoutfd, util.stdout.fileno())
1098 1101 os.dup2(stderrfd, util.stderr.fileno())
1099 1102 pager.stdin.close()
1100 1103 pager.wait()
1101 1104
1102 1105 return True
1103 1106
1104 1107 @property
1105 1108 def _exithandlers(self):
1106 1109 return _reqexithandlers
1107 1110
1108 1111 def atexit(self, func, *args, **kwargs):
1109 1112 '''register a function to run after dispatching a request
1110 1113
1111 1114 Handlers do not stay registered across request boundaries.'''
1112 1115 self._exithandlers.append((func, args, kwargs))
1113 1116 return func
1114 1117
1115 1118 def interface(self, feature):
1116 1119 """what interface to use for interactive console features?
1117 1120
1118 1121 The interface is controlled by the value of `ui.interface` but also by
1119 1122 the value of feature-specific configuration. For example:
1120 1123
1121 1124 ui.interface.histedit = text
1122 1125 ui.interface.chunkselector = curses
1123 1126
1124 1127 Here the features are "histedit" and "chunkselector".
1125 1128
1126 1129 The configuration above means that the default interfaces for commands
1127 1130 is curses, the interface for histedit is text and the interface for
1128 1131 selecting chunk is crecord (the best curses interface available).
1129 1132
1130 1133 Consider the following example:
1131 1134 ui.interface = curses
1132 1135 ui.interface.histedit = text
1133 1136
1134 1137 Then histedit will use the text interface and chunkselector will use
1135 1138 the default curses interface (crecord at the moment).
1136 1139 """
1137 1140 alldefaults = frozenset(["text", "curses"])
1138 1141
1139 1142 featureinterfaces = {
1140 1143 "chunkselector": [
1141 1144 "text",
1142 1145 "curses",
1143 1146 ]
1144 1147 }
1145 1148
1146 1149 # Feature-specific interface
1147 1150 if feature not in featureinterfaces.keys():
1148 1151 # Programming error, not user error
1149 1152 raise ValueError("Unknown feature requested %s" % feature)
1150 1153
1151 1154 availableinterfaces = frozenset(featureinterfaces[feature])
1152 1155 if alldefaults > availableinterfaces:
1153 1156 # Programming error, not user error. We need a use case to
1154 1157 # define the right thing to do here.
1155 1158 raise ValueError(
1156 1159 "Feature %s does not handle all default interfaces" %
1157 1160 feature)
1158 1161
1159 1162 if self.plain():
1160 1163 return "text"
1161 1164
1162 1165 # Default interface for all the features
1163 1166 defaultinterface = "text"
1164 1167 i = self.config("ui", "interface")
1165 1168 if i in alldefaults:
1166 1169 defaultinterface = i
1167 1170
1168 1171 choseninterface = defaultinterface
1169 1172 f = self.config("ui", "interface.%s" % feature)
1170 1173 if f in availableinterfaces:
1171 1174 choseninterface = f
1172 1175
1173 1176 if i is not None and defaultinterface != i:
1174 1177 if f is not None:
1175 1178 self.warn(_("invalid value for ui.interface: %s\n") %
1176 1179 (i,))
1177 1180 else:
1178 1181 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1179 1182 (i, choseninterface))
1180 1183 if f is not None and choseninterface != f:
1181 1184 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1182 1185 (feature, f, choseninterface))
1183 1186
1184 1187 return choseninterface
1185 1188
1186 1189 def interactive(self):
1187 1190 '''is interactive input allowed?
1188 1191
1189 1192 An interactive session is a session where input can be reasonably read
1190 1193 from `sys.stdin'. If this function returns false, any attempt to read
1191 1194 from stdin should fail with an error, unless a sensible default has been
1192 1195 specified.
1193 1196
1194 1197 Interactiveness is triggered by the value of the `ui.interactive'
1195 1198 configuration variable or - if it is unset - when `sys.stdin' points
1196 1199 to a terminal device.
1197 1200
1198 1201 This function refers to input only; for output, see `ui.formatted()'.
1199 1202 '''
1200 1203 i = self.configbool("ui", "interactive")
1201 1204 if i is None:
1202 1205 # some environments replace stdin without implementing isatty
1203 1206 # usually those are non-interactive
1204 1207 return self._isatty(self.fin)
1205 1208
1206 1209 return i
1207 1210
1208 1211 def termwidth(self):
1209 1212 '''how wide is the terminal in columns?
1210 1213 '''
1211 1214 if 'COLUMNS' in encoding.environ:
1212 1215 try:
1213 1216 return int(encoding.environ['COLUMNS'])
1214 1217 except ValueError:
1215 1218 pass
1216 1219 return scmutil.termsize(self)[0]
1217 1220
1218 1221 def formatted(self):
1219 1222 '''should formatted output be used?
1220 1223
1221 1224 It is often desirable to format the output to suite the output medium.
1222 1225 Examples of this are truncating long lines or colorizing messages.
1223 1226 However, this is not often not desirable when piping output into other
1224 1227 utilities, e.g. `grep'.
1225 1228
1226 1229 Formatted output is triggered by the value of the `ui.formatted'
1227 1230 configuration variable or - if it is unset - when `sys.stdout' points
1228 1231 to a terminal device. Please note that `ui.formatted' should be
1229 1232 considered an implementation detail; it is not intended for use outside
1230 1233 Mercurial or its extensions.
1231 1234
1232 1235 This function refers to output only; for input, see `ui.interactive()'.
1233 1236 This function always returns false when in plain mode, see `ui.plain()'.
1234 1237 '''
1235 1238 if self.plain():
1236 1239 return False
1237 1240
1238 1241 i = self.configbool("ui", "formatted")
1239 1242 if i is None:
1240 1243 # some environments replace stdout without implementing isatty
1241 1244 # usually those are non-interactive
1242 1245 return self._isatty(self.fout)
1243 1246
1244 1247 return i
1245 1248
1246 1249 def _readline(self, prompt=''):
1247 1250 if self._isatty(self.fin):
1248 1251 try:
1249 1252 # magically add command line editing support, where
1250 1253 # available
1251 1254 import readline
1252 1255 # force demandimport to really load the module
1253 1256 readline.read_history_file
1254 1257 # windows sometimes raises something other than ImportError
1255 1258 except Exception:
1256 1259 pass
1257 1260
1258 1261 # call write() so output goes through subclassed implementation
1259 1262 # e.g. color extension on Windows
1260 1263 self.write(prompt, prompt=True)
1261 1264 self.flush()
1262 1265
1263 1266 # prompt ' ' must exist; otherwise readline may delete entire line
1264 1267 # - http://bugs.python.org/issue12833
1265 1268 with self.timeblockedsection('stdio'):
1266 1269 line = util.bytesinput(self.fin, self.fout, r' ')
1267 1270
1268 1271 # When stdin is in binary mode on Windows, it can cause
1269 1272 # raw_input() to emit an extra trailing carriage return
1270 1273 if pycompat.oslinesep == '\r\n' and line and line[-1] == '\r':
1271 1274 line = line[:-1]
1272 1275 return line
1273 1276
1274 1277 def prompt(self, msg, default="y"):
1275 1278 """Prompt user with msg, read response.
1276 1279 If ui is not interactive, the default is returned.
1277 1280 """
1278 1281 if not self.interactive():
1279 1282 self.write(msg, ' ', default or '', "\n")
1280 1283 return default
1281 1284 try:
1282 1285 r = self._readline(self.label(msg, 'ui.prompt'))
1283 1286 if not r:
1284 1287 r = default
1285 1288 if self.configbool('ui', 'promptecho'):
1286 1289 self.write(r, "\n")
1287 1290 return r
1288 1291 except EOFError:
1289 1292 raise error.ResponseExpected()
1290 1293
1291 1294 @staticmethod
1292 1295 def extractchoices(prompt):
1293 1296 """Extract prompt message and list of choices from specified prompt.
1294 1297
1295 1298 This returns tuple "(message, choices)", and "choices" is the
1296 1299 list of tuple "(response character, text without &)".
1297 1300
1298 1301 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1299 1302 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1300 1303 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1301 1304 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1302 1305 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1303 1306 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1304 1307 """
1305 1308
1306 1309 # Sadly, the prompt string may have been built with a filename
1307 1310 # containing "$$" so let's try to find the first valid-looking
1308 1311 # prompt to start parsing. Sadly, we also can't rely on
1309 1312 # choices containing spaces, ASCII, or basically anything
1310 1313 # except an ampersand followed by a character.
1311 1314 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1312 1315 msg = m.group(1)
1313 1316 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1314 1317 def choicetuple(s):
1315 1318 ampidx = s.index('&')
1316 1319 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1317 1320 return (msg, [choicetuple(s) for s in choices])
1318 1321
1319 1322 def promptchoice(self, prompt, default=0):
1320 1323 """Prompt user with a message, read response, and ensure it matches
1321 1324 one of the provided choices. The prompt is formatted as follows:
1322 1325
1323 1326 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1324 1327
1325 1328 The index of the choice is returned. Responses are case
1326 1329 insensitive. If ui is not interactive, the default is
1327 1330 returned.
1328 1331 """
1329 1332
1330 1333 msg, choices = self.extractchoices(prompt)
1331 1334 resps = [r for r, t in choices]
1332 1335 while True:
1333 1336 r = self.prompt(msg, resps[default])
1334 1337 if r.lower() in resps:
1335 1338 return resps.index(r.lower())
1336 1339 self.write(_("unrecognized response\n"))
1337 1340
1338 1341 def getpass(self, prompt=None, default=None):
1339 1342 if not self.interactive():
1340 1343 return default
1341 1344 try:
1342 1345 self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
1343 1346 # disable getpass() only if explicitly specified. it's still valid
1344 1347 # to interact with tty even if fin is not a tty.
1345 1348 with self.timeblockedsection('stdio'):
1346 1349 if self.configbool('ui', 'nontty'):
1347 1350 l = self.fin.readline()
1348 1351 if not l:
1349 1352 raise EOFError
1350 1353 return l.rstrip('\n')
1351 1354 else:
1352 1355 return getpass.getpass('')
1353 1356 except EOFError:
1354 1357 raise error.ResponseExpected()
1355 1358 def status(self, *msg, **opts):
1356 1359 '''write status message to output (if ui.quiet is False)
1357 1360
1358 1361 This adds an output label of "ui.status".
1359 1362 '''
1360 1363 if not self.quiet:
1361 1364 opts[r'label'] = opts.get(r'label', '') + ' ui.status'
1362 1365 self.write(*msg, **opts)
1363 1366 def warn(self, *msg, **opts):
1364 1367 '''write warning message to output (stderr)
1365 1368
1366 1369 This adds an output label of "ui.warning".
1367 1370 '''
1368 1371 opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
1369 1372 self.write_err(*msg, **opts)
1370 1373 def note(self, *msg, **opts):
1371 1374 '''write note to output (if ui.verbose is True)
1372 1375
1373 1376 This adds an output label of "ui.note".
1374 1377 '''
1375 1378 if self.verbose:
1376 1379 opts[r'label'] = opts.get(r'label', '') + ' ui.note'
1377 1380 self.write(*msg, **opts)
1378 1381 def debug(self, *msg, **opts):
1379 1382 '''write debug message to output (if ui.debugflag is True)
1380 1383
1381 1384 This adds an output label of "ui.debug".
1382 1385 '''
1383 1386 if self.debugflag:
1384 1387 opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
1385 1388 self.write(*msg, **opts)
1386 1389
1387 1390 def edit(self, text, user, extra=None, editform=None, pending=None,
1388 1391 repopath=None, action=None):
1389 1392 if action is None:
1390 1393 self.develwarn('action is None but will soon be a required '
1391 1394 'parameter to ui.edit()')
1392 1395 extra_defaults = {
1393 1396 'prefix': 'editor',
1394 1397 'suffix': '.txt',
1395 1398 }
1396 1399 if extra is not None:
1397 1400 if extra.get('suffix') is not None:
1398 1401 self.develwarn('extra.suffix is not None but will soon be '
1399 1402 'ignored by ui.edit()')
1400 1403 extra_defaults.update(extra)
1401 1404 extra = extra_defaults
1402 1405
1403 1406 if action == 'diff':
1404 1407 suffix = '.diff'
1405 1408 elif action:
1406 1409 suffix = '.%s.hg.txt' % action
1407 1410 else:
1408 1411 suffix = extra['suffix']
1409 1412
1410 1413 rdir = None
1411 1414 if self.configbool('experimental', 'editortmpinhg'):
1412 1415 rdir = repopath
1413 1416 (fd, name) = tempfile.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1414 1417 suffix=suffix,
1415 1418 dir=rdir)
1416 1419 try:
1417 1420 f = os.fdopen(fd, r'wb')
1418 1421 f.write(util.tonativeeol(text))
1419 1422 f.close()
1420 1423
1421 1424 environ = {'HGUSER': user}
1422 1425 if 'transplant_source' in extra:
1423 1426 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1424 1427 for label in ('intermediate-source', 'source', 'rebase_source'):
1425 1428 if label in extra:
1426 1429 environ.update({'HGREVISION': extra[label]})
1427 1430 break
1428 1431 if editform:
1429 1432 environ.update({'HGEDITFORM': editform})
1430 1433 if pending:
1431 1434 environ.update({'HG_PENDING': pending})
1432 1435
1433 1436 editor = self.geteditor()
1434 1437
1435 1438 self.system("%s \"%s\"" % (editor, name),
1436 1439 environ=environ,
1437 1440 onerr=error.Abort, errprefix=_("edit failed"),
1438 1441 blockedtag='editor')
1439 1442
1440 1443 f = open(name, r'rb')
1441 1444 t = util.fromnativeeol(f.read())
1442 1445 f.close()
1443 1446 finally:
1444 1447 os.unlink(name)
1445 1448
1446 1449 return t
1447 1450
1448 1451 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1449 1452 blockedtag=None):
1450 1453 '''execute shell command with appropriate output stream. command
1451 1454 output will be redirected if fout is not stdout.
1452 1455
1453 1456 if command fails and onerr is None, return status, else raise onerr
1454 1457 object as exception.
1455 1458 '''
1456 1459 if blockedtag is None:
1457 1460 # Long cmds tend to be because of an absolute path on cmd. Keep
1458 1461 # the tail end instead
1459 1462 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1460 1463 blockedtag = 'unknown_system_' + cmdsuffix
1461 1464 out = self.fout
1462 1465 if any(s[1] for s in self._bufferstates):
1463 1466 out = self
1464 1467 with self.timeblockedsection(blockedtag):
1465 1468 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1466 1469 if rc and onerr:
1467 1470 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1468 1471 util.explainexit(rc)[0])
1469 1472 if errprefix:
1470 1473 errmsg = '%s: %s' % (errprefix, errmsg)
1471 1474 raise onerr(errmsg)
1472 1475 return rc
1473 1476
1474 1477 def _runsystem(self, cmd, environ, cwd, out):
1475 1478 """actually execute the given shell command (can be overridden by
1476 1479 extensions like chg)"""
1477 1480 return util.system(cmd, environ=environ, cwd=cwd, out=out)
1478 1481
1479 1482 def traceback(self, exc=None, force=False):
1480 1483 '''print exception traceback if traceback printing enabled or forced.
1481 1484 only to call in exception handler. returns true if traceback
1482 1485 printed.'''
1483 1486 if self.tracebackflag or force:
1484 1487 if exc is None:
1485 1488 exc = sys.exc_info()
1486 1489 cause = getattr(exc[1], 'cause', None)
1487 1490
1488 1491 if cause is not None:
1489 1492 causetb = traceback.format_tb(cause[2])
1490 1493 exctb = traceback.format_tb(exc[2])
1491 1494 exconly = traceback.format_exception_only(cause[0], cause[1])
1492 1495
1493 1496 # exclude frame where 'exc' was chained and rethrown from exctb
1494 1497 self.write_err('Traceback (most recent call last):\n',
1495 1498 ''.join(exctb[:-1]),
1496 1499 ''.join(causetb),
1497 1500 ''.join(exconly))
1498 1501 else:
1499 1502 output = traceback.format_exception(exc[0], exc[1], exc[2])
1500 1503 data = r''.join(output)
1501 1504 if pycompat.ispy3:
1502 1505 enc = pycompat.sysstr(encoding.encoding)
1503 1506 data = data.encode(enc, errors=r'replace')
1504 1507 self.write_err(data)
1505 1508 return self.tracebackflag or force
1506 1509
1507 1510 def geteditor(self):
1508 1511 '''return editor to use'''
1509 1512 if pycompat.sysplatform == 'plan9':
1510 1513 # vi is the MIPS instruction simulator on Plan 9. We
1511 1514 # instead default to E to plumb commit messages to
1512 1515 # avoid confusion.
1513 1516 editor = 'E'
1514 1517 else:
1515 1518 editor = 'vi'
1516 1519 return (encoding.environ.get("HGEDITOR") or
1517 1520 self.config("ui", "editor", editor))
1518 1521
1519 1522 @util.propertycache
1520 1523 def _progbar(self):
1521 1524 """setup the progbar singleton to the ui object"""
1522 1525 if (self.quiet or self.debugflag
1523 1526 or self.configbool('progress', 'disable')
1524 1527 or not progress.shouldprint(self)):
1525 1528 return None
1526 1529 return getprogbar(self)
1527 1530
1528 1531 def _progclear(self):
1529 1532 """clear progress bar output if any. use it before any output"""
1530 1533 if not haveprogbar(): # nothing loaded yet
1531 1534 return
1532 1535 if self._progbar is not None and self._progbar.printed:
1533 1536 self._progbar.clear()
1534 1537
1535 1538 def progress(self, topic, pos, item="", unit="", total=None):
1536 1539 '''show a progress message
1537 1540
1538 1541 By default a textual progress bar will be displayed if an operation
1539 1542 takes too long. 'topic' is the current operation, 'item' is a
1540 1543 non-numeric marker of the current position (i.e. the currently
1541 1544 in-process file), 'pos' is the current numeric position (i.e.
1542 1545 revision, bytes, etc.), unit is a corresponding unit label,
1543 1546 and total is the highest expected pos.
1544 1547
1545 1548 Multiple nested topics may be active at a time.
1546 1549
1547 1550 All topics should be marked closed by setting pos to None at
1548 1551 termination.
1549 1552 '''
1550 1553 if self._progbar is not None:
1551 1554 self._progbar.progress(topic, pos, item=item, unit=unit,
1552 1555 total=total)
1553 1556 if pos is None or not self.configbool('progress', 'debug'):
1554 1557 return
1555 1558
1556 1559 if unit:
1557 1560 unit = ' ' + unit
1558 1561 if item:
1559 1562 item = ' ' + item
1560 1563
1561 1564 if total:
1562 1565 pct = 100.0 * pos / total
1563 1566 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1564 1567 % (topic, item, pos, total, unit, pct))
1565 1568 else:
1566 1569 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1567 1570
1568 1571 def log(self, service, *msg, **opts):
1569 1572 '''hook for logging facility extensions
1570 1573
1571 1574 service should be a readily-identifiable subsystem, which will
1572 1575 allow filtering.
1573 1576
1574 1577 *msg should be a newline-terminated format string to log, and
1575 1578 then any values to %-format into that format string.
1576 1579
1577 1580 **opts currently has no defined meanings.
1578 1581 '''
1579 1582
1580 1583 def label(self, msg, label):
1581 1584 '''style msg based on supplied label
1582 1585
1583 1586 If some color mode is enabled, this will add the necessary control
1584 1587 characters to apply such color. In addition, 'debug' color mode adds
1585 1588 markup showing which label affects a piece of text.
1586 1589
1587 1590 ui.write(s, 'label') is equivalent to
1588 1591 ui.write(ui.label(s, 'label')).
1589 1592 '''
1590 1593 if self._colormode is not None:
1591 1594 return color.colorlabel(self, msg, label)
1592 1595 return msg
1593 1596
1594 1597 def develwarn(self, msg, stacklevel=1, config=None):
1595 1598 """issue a developer warning message
1596 1599
1597 1600 Use 'stacklevel' to report the offender some layers further up in the
1598 1601 stack.
1599 1602 """
1600 1603 if not self.configbool('devel', 'all-warnings'):
1601 1604 if config is not None and not self.configbool('devel', config):
1602 1605 return
1603 1606 msg = 'devel-warn: ' + msg
1604 1607 stacklevel += 1 # get in develwarn
1605 1608 if self.tracebackflag:
1606 1609 util.debugstacktrace(msg, stacklevel, self.ferr, self.fout)
1607 1610 self.log('develwarn', '%s at:\n%s' %
1608 1611 (msg, ''.join(util.getstackframes(stacklevel))))
1609 1612 else:
1610 1613 curframe = inspect.currentframe()
1611 1614 calframe = inspect.getouterframes(curframe, 2)
1612 1615 self.write_err('%s at: %s:%s (%s)\n'
1613 1616 % ((msg,) + calframe[stacklevel][1:4]))
1614 1617 self.log('develwarn', '%s at: %s:%s (%s)\n',
1615 1618 msg, *calframe[stacklevel][1:4])
1616 1619 curframe = calframe = None # avoid cycles
1617 1620
1618 1621 def deprecwarn(self, msg, version):
1619 1622 """issue a deprecation warning
1620 1623
1621 1624 - msg: message explaining what is deprecated and how to upgrade,
1622 1625 - version: last version where the API will be supported,
1623 1626 """
1624 1627 if not (self.configbool('devel', 'all-warnings')
1625 1628 or self.configbool('devel', 'deprec-warn')):
1626 1629 return
1627 1630 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1628 1631 " update your code.)") % version
1629 1632 self.develwarn(msg, stacklevel=2, config='deprec-warn')
1630 1633
1631 1634 def exportableenviron(self):
1632 1635 """The environment variables that are safe to export, e.g. through
1633 1636 hgweb.
1634 1637 """
1635 1638 return self._exportableenviron
1636 1639
1637 1640 @contextlib.contextmanager
1638 1641 def configoverride(self, overrides, source=""):
1639 1642 """Context manager for temporary config overrides
1640 1643 `overrides` must be a dict of the following structure:
1641 1644 {(section, name) : value}"""
1642 1645 backups = {}
1643 1646 try:
1644 1647 for (section, name), value in overrides.items():
1645 1648 backups[(section, name)] = self.backupconfig(section, name)
1646 1649 self.setconfig(section, name, value, source)
1647 1650 yield
1648 1651 finally:
1649 1652 for __, backup in backups.items():
1650 1653 self.restoreconfig(backup)
1651 1654 # just restoring ui.quiet config to the previous value is not enough
1652 1655 # as it does not update ui.quiet class member
1653 1656 if ('ui', 'quiet') in overrides:
1654 1657 self.fixconfig(section='ui')
1655 1658
1656 1659 class paths(dict):
1657 1660 """Represents a collection of paths and their configs.
1658 1661
1659 1662 Data is initially derived from ui instances and the config files they have
1660 1663 loaded.
1661 1664 """
1662 1665 def __init__(self, ui):
1663 1666 dict.__init__(self)
1664 1667
1665 1668 for name, loc in ui.configitems('paths', ignoresub=True):
1666 1669 # No location is the same as not existing.
1667 1670 if not loc:
1668 1671 continue
1669 1672 loc, sub = ui.configsuboptions('paths', name)
1670 1673 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1671 1674
1672 1675 def getpath(self, name, default=None):
1673 1676 """Return a ``path`` from a string, falling back to default.
1674 1677
1675 1678 ``name`` can be a named path or locations. Locations are filesystem
1676 1679 paths or URIs.
1677 1680
1678 1681 Returns None if ``name`` is not a registered path, a URI, or a local
1679 1682 path to a repo.
1680 1683 """
1681 1684 # Only fall back to default if no path was requested.
1682 1685 if name is None:
1683 1686 if not default:
1684 1687 default = ()
1685 1688 elif not isinstance(default, (tuple, list)):
1686 1689 default = (default,)
1687 1690 for k in default:
1688 1691 try:
1689 1692 return self[k]
1690 1693 except KeyError:
1691 1694 continue
1692 1695 return None
1693 1696
1694 1697 # Most likely empty string.
1695 1698 # This may need to raise in the future.
1696 1699 if not name:
1697 1700 return None
1698 1701
1699 1702 try:
1700 1703 return self[name]
1701 1704 except KeyError:
1702 1705 # Try to resolve as a local path or URI.
1703 1706 try:
1704 1707 # We don't pass sub-options in, so no need to pass ui instance.
1705 1708 return path(None, None, rawloc=name)
1706 1709 except ValueError:
1707 1710 raise error.RepoError(_('repository %s does not exist') %
1708 1711 name)
1709 1712
1710 1713 _pathsuboptions = {}
1711 1714
1712 1715 def pathsuboption(option, attr):
1713 1716 """Decorator used to declare a path sub-option.
1714 1717
1715 1718 Arguments are the sub-option name and the attribute it should set on
1716 1719 ``path`` instances.
1717 1720
1718 1721 The decorated function will receive as arguments a ``ui`` instance,
1719 1722 ``path`` instance, and the string value of this option from the config.
1720 1723 The function should return the value that will be set on the ``path``
1721 1724 instance.
1722 1725
1723 1726 This decorator can be used to perform additional verification of
1724 1727 sub-options and to change the type of sub-options.
1725 1728 """
1726 1729 def register(func):
1727 1730 _pathsuboptions[option] = (attr, func)
1728 1731 return func
1729 1732 return register
1730 1733
1731 1734 @pathsuboption('pushurl', 'pushloc')
1732 1735 def pushurlpathoption(ui, path, value):
1733 1736 u = util.url(value)
1734 1737 # Actually require a URL.
1735 1738 if not u.scheme:
1736 1739 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1737 1740 return None
1738 1741
1739 1742 # Don't support the #foo syntax in the push URL to declare branch to
1740 1743 # push.
1741 1744 if u.fragment:
1742 1745 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1743 1746 'ignoring)\n') % path.name)
1744 1747 u.fragment = None
1745 1748
1746 1749 return str(u)
1747 1750
1748 1751 @pathsuboption('pushrev', 'pushrev')
1749 1752 def pushrevpathoption(ui, path, value):
1750 1753 return value
1751 1754
1752 1755 class path(object):
1753 1756 """Represents an individual path and its configuration."""
1754 1757
1755 1758 def __init__(self, ui, name, rawloc=None, suboptions=None):
1756 1759 """Construct a path from its config options.
1757 1760
1758 1761 ``ui`` is the ``ui`` instance the path is coming from.
1759 1762 ``name`` is the symbolic name of the path.
1760 1763 ``rawloc`` is the raw location, as defined in the config.
1761 1764 ``pushloc`` is the raw locations pushes should be made to.
1762 1765
1763 1766 If ``name`` is not defined, we require that the location be a) a local
1764 1767 filesystem path with a .hg directory or b) a URL. If not,
1765 1768 ``ValueError`` is raised.
1766 1769 """
1767 1770 if not rawloc:
1768 1771 raise ValueError('rawloc must be defined')
1769 1772
1770 1773 # Locations may define branches via syntax <base>#<branch>.
1771 1774 u = util.url(rawloc)
1772 1775 branch = None
1773 1776 if u.fragment:
1774 1777 branch = u.fragment
1775 1778 u.fragment = None
1776 1779
1777 1780 self.url = u
1778 1781 self.branch = branch
1779 1782
1780 1783 self.name = name
1781 1784 self.rawloc = rawloc
1782 1785 self.loc = '%s' % u
1783 1786
1784 1787 # When given a raw location but not a symbolic name, validate the
1785 1788 # location is valid.
1786 1789 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1787 1790 raise ValueError('location is not a URL or path to a local '
1788 1791 'repo: %s' % rawloc)
1789 1792
1790 1793 suboptions = suboptions or {}
1791 1794
1792 1795 # Now process the sub-options. If a sub-option is registered, its
1793 1796 # attribute will always be present. The value will be None if there
1794 1797 # was no valid sub-option.
1795 1798 for suboption, (attr, func) in _pathsuboptions.iteritems():
1796 1799 if suboption not in suboptions:
1797 1800 setattr(self, attr, None)
1798 1801 continue
1799 1802
1800 1803 value = func(ui, self, suboptions[suboption])
1801 1804 setattr(self, attr, value)
1802 1805
1803 1806 def _isvalidlocalpath(self, path):
1804 1807 """Returns True if the given path is a potentially valid repository.
1805 1808 This is its own function so that extensions can change the definition of
1806 1809 'valid' in this case (like when pulling from a git repo into a hg
1807 1810 one)."""
1808 1811 return os.path.isdir(os.path.join(path, '.hg'))
1809 1812
1810 1813 @property
1811 1814 def suboptions(self):
1812 1815 """Return sub-options and their values for this path.
1813 1816
1814 1817 This is intended to be used for presentation purposes.
1815 1818 """
1816 1819 d = {}
1817 1820 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1818 1821 value = getattr(self, attr)
1819 1822 if value is not None:
1820 1823 d[subopt] = value
1821 1824 return d
1822 1825
1823 1826 # we instantiate one globally shared progress bar to avoid
1824 1827 # competing progress bars when multiple UI objects get created
1825 1828 _progresssingleton = None
1826 1829
1827 1830 def getprogbar(ui):
1828 1831 global _progresssingleton
1829 1832 if _progresssingleton is None:
1830 1833 # passing 'ui' object to the singleton is fishy,
1831 1834 # this is how the extension used to work but feel free to rework it.
1832 1835 _progresssingleton = progress.progbar(ui)
1833 1836 return _progresssingleton
1834 1837
1835 1838 def haveprogbar():
1836 1839 return _progresssingleton is not None
General Comments 0
You need to be logged in to leave comments. Login now