##// END OF EJS Templates
formatter: fix handling of None value in templater mapping...
Yuya Nishihara -
r43645:7e20b705 stable
parent child Browse files
Show More
@@ -1,839 +1,843 b''
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 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 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.verbose = verbose
49 49 ... ui.pushbuffer()
50 50 ... try:
51 51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 52 ... pycompat.byteskwargs(opts)))
53 53 ... finally:
54 54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55 55
56 56 Basic example:
57 57
58 58 >>> def files(ui, fm):
59 59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 60 ... for f in files:
61 61 ... fm.startitem()
62 62 ... fm.write(b'path', b'%s', f[0])
63 63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 65 ... fm.data(size=f[1])
66 66 ... fm.plain(b'\\n')
67 67 ... fm.end()
68 68 >>> show(files)
69 69 foo
70 70 bar
71 71 >>> show(files, verbose=True)
72 72 foo 1970-01-01 00:00:00
73 73 bar 1970-01-01 00:00:01
74 74 >>> show(files, template=b'json')
75 75 [
76 76 {
77 77 "date": [0, 0],
78 78 "path": "foo",
79 79 "size": 123
80 80 },
81 81 {
82 82 "date": [1, 0],
83 83 "path": "bar",
84 84 "size": 456
85 85 }
86 86 ]
87 87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 88 path: foo
89 89 date: 1970-01-01T00:00:00+00:00
90 90 path: bar
91 91 date: 1970-01-01T00:00:01+00:00
92 92
93 93 Nested example:
94 94
95 95 >>> def subrepos(ui, fm):
96 96 ... fm.startitem()
97 97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99 99 ... fm.end()
100 100 >>> show(subrepos)
101 101 [baz]
102 102 foo
103 103 bar
104 104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 105 baz: foo, bar
106 106 """
107 107
108 108 from __future__ import absolute_import, print_function
109 109
110 110 import contextlib
111 111 import itertools
112 112 import os
113 113
114 114 from .i18n import _
115 115 from .node import (
116 116 hex,
117 117 short,
118 118 )
119 119 from .thirdparty import attr
120 120
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 templatefilters,
125 125 templatekw,
126 126 templater,
127 127 templateutil,
128 128 util,
129 129 )
130 130 from .utils import (
131 131 cborutil,
132 132 dateutil,
133 133 stringutil,
134 134 )
135 135
136 136 pickle = util.pickle
137 137
138 138
139 139 def isprintable(obj):
140 140 """Check if the given object can be directly passed in to formatter's
141 141 write() and data() functions
142 142
143 143 Returns False if the object is unsupported or must be pre-processed by
144 144 formatdate(), formatdict(), or formatlist().
145 145 """
146 146 return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
147 147
148 148
149 149 class _nullconverter(object):
150 150 '''convert non-primitive data types to be processed by formatter'''
151 151
152 152 # set to True if context object should be stored as item
153 153 storecontext = False
154 154
155 155 @staticmethod
156 156 def wrapnested(data, tmpl, sep):
157 157 '''wrap nested data by appropriate type'''
158 158 return data
159 159
160 160 @staticmethod
161 161 def formatdate(date, fmt):
162 162 '''convert date tuple to appropriate format'''
163 163 # timestamp can be float, but the canonical form should be int
164 164 ts, tz = date
165 165 return (int(ts), tz)
166 166
167 167 @staticmethod
168 168 def formatdict(data, key, value, fmt, sep):
169 169 '''convert dict or key-value pairs to appropriate dict format'''
170 170 # use plain dict instead of util.sortdict so that data can be
171 171 # serialized as a builtin dict in pickle output
172 172 return dict(data)
173 173
174 174 @staticmethod
175 175 def formatlist(data, name, fmt, sep):
176 176 '''convert iterable to appropriate list format'''
177 177 return list(data)
178 178
179 179
180 180 class baseformatter(object):
181 181 def __init__(self, ui, topic, opts, converter):
182 182 self._ui = ui
183 183 self._topic = topic
184 184 self._opts = opts
185 185 self._converter = converter
186 186 self._item = None
187 187 # function to convert node to string suitable for this output
188 188 self.hexfunc = hex
189 189
190 190 def __enter__(self):
191 191 return self
192 192
193 193 def __exit__(self, exctype, excvalue, traceback):
194 194 if exctype is None:
195 195 self.end()
196 196
197 197 def _showitem(self):
198 198 '''show a formatted item once all data is collected'''
199 199
200 200 def startitem(self):
201 201 '''begin an item in the format list'''
202 202 if self._item is not None:
203 203 self._showitem()
204 204 self._item = {}
205 205
206 206 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
207 207 '''convert date tuple to appropriate format'''
208 208 return self._converter.formatdate(date, fmt)
209 209
210 210 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
211 211 '''convert dict or key-value pairs to appropriate dict format'''
212 212 return self._converter.formatdict(data, key, value, fmt, sep)
213 213
214 214 def formatlist(self, data, name, fmt=None, sep=b' '):
215 215 '''convert iterable to appropriate list format'''
216 216 # name is mandatory argument for now, but it could be optional if
217 217 # we have default template keyword, e.g. {item}
218 218 return self._converter.formatlist(data, name, fmt, sep)
219 219
220 220 def context(self, **ctxs):
221 221 '''insert context objects to be used to render template keywords'''
222 222 ctxs = pycompat.byteskwargs(ctxs)
223 223 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
224 224 if self._converter.storecontext:
225 225 # populate missing resources in fctx -> ctx -> repo order
226 226 if b'fctx' in ctxs and b'ctx' not in ctxs:
227 227 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
228 228 if b'ctx' in ctxs and b'repo' not in ctxs:
229 229 ctxs[b'repo'] = ctxs[b'ctx'].repo()
230 230 self._item.update(ctxs)
231 231
232 232 def datahint(self):
233 233 '''set of field names to be referenced'''
234 234 return set()
235 235
236 236 def data(self, **data):
237 237 '''insert data into item that's not shown in default output'''
238 238 data = pycompat.byteskwargs(data)
239 239 self._item.update(data)
240 240
241 241 def write(self, fields, deftext, *fielddata, **opts):
242 242 '''do default text output while assigning data to item'''
243 243 fieldkeys = fields.split()
244 244 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
245 245 self._item.update(zip(fieldkeys, fielddata))
246 246
247 247 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
248 248 '''do conditional write (primarily for plain formatter)'''
249 249 fieldkeys = fields.split()
250 250 assert len(fieldkeys) == len(fielddata)
251 251 self._item.update(zip(fieldkeys, fielddata))
252 252
253 253 def plain(self, text, **opts):
254 254 '''show raw text for non-templated mode'''
255 255
256 256 def isplain(self):
257 257 '''check for plain formatter usage'''
258 258 return False
259 259
260 260 def nested(self, field, tmpl=None, sep=b''):
261 261 '''sub formatter to store nested data in the specified field'''
262 262 data = []
263 263 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
264 264 return _nestedformatter(self._ui, self._converter, data)
265 265
266 266 def end(self):
267 267 '''end output for the formatter'''
268 268 if self._item is not None:
269 269 self._showitem()
270 270
271 271
272 272 def nullformatter(ui, topic, opts):
273 273 '''formatter that prints nothing'''
274 274 return baseformatter(ui, topic, opts, converter=_nullconverter)
275 275
276 276
277 277 class _nestedformatter(baseformatter):
278 278 '''build sub items and store them in the parent formatter'''
279 279
280 280 def __init__(self, ui, converter, data):
281 281 baseformatter.__init__(
282 282 self, ui, topic=b'', opts={}, converter=converter
283 283 )
284 284 self._data = data
285 285
286 286 def _showitem(self):
287 287 self._data.append(self._item)
288 288
289 289
290 290 def _iteritems(data):
291 291 '''iterate key-value pairs in stable order'''
292 292 if isinstance(data, dict):
293 293 return sorted(pycompat.iteritems(data))
294 294 return data
295 295
296 296
297 297 class _plainconverter(object):
298 298 '''convert non-primitive data types to text'''
299 299
300 300 storecontext = False
301 301
302 302 @staticmethod
303 303 def wrapnested(data, tmpl, sep):
304 304 raise error.ProgrammingError(b'plainformatter should never be nested')
305 305
306 306 @staticmethod
307 307 def formatdate(date, fmt):
308 308 '''stringify date tuple in the given format'''
309 309 return dateutil.datestr(date, fmt)
310 310
311 311 @staticmethod
312 312 def formatdict(data, key, value, fmt, sep):
313 313 '''stringify key-value pairs separated by sep'''
314 314 prefmt = pycompat.identity
315 315 if fmt is None:
316 316 fmt = b'%s=%s'
317 317 prefmt = pycompat.bytestr
318 318 return sep.join(
319 319 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
320 320 )
321 321
322 322 @staticmethod
323 323 def formatlist(data, name, fmt, sep):
324 324 '''stringify iterable separated by sep'''
325 325 prefmt = pycompat.identity
326 326 if fmt is None:
327 327 fmt = b'%s'
328 328 prefmt = pycompat.bytestr
329 329 return sep.join(fmt % prefmt(e) for e in data)
330 330
331 331
332 332 class plainformatter(baseformatter):
333 333 '''the default text output scheme'''
334 334
335 335 def __init__(self, ui, out, topic, opts):
336 336 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
337 337 if ui.debugflag:
338 338 self.hexfunc = hex
339 339 else:
340 340 self.hexfunc = short
341 341 if ui is out:
342 342 self._write = ui.write
343 343 else:
344 344 self._write = lambda s, **opts: out.write(s)
345 345
346 346 def startitem(self):
347 347 pass
348 348
349 349 def data(self, **data):
350 350 pass
351 351
352 352 def write(self, fields, deftext, *fielddata, **opts):
353 353 self._write(deftext % fielddata, **opts)
354 354
355 355 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
356 356 '''do conditional write'''
357 357 if cond:
358 358 self._write(deftext % fielddata, **opts)
359 359
360 360 def plain(self, text, **opts):
361 361 self._write(text, **opts)
362 362
363 363 def isplain(self):
364 364 return True
365 365
366 366 def nested(self, field, tmpl=None, sep=b''):
367 367 # nested data will be directly written to ui
368 368 return self
369 369
370 370 def end(self):
371 371 pass
372 372
373 373
374 374 class debugformatter(baseformatter):
375 375 def __init__(self, ui, out, topic, opts):
376 376 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
377 377 self._out = out
378 378 self._out.write(b"%s = [\n" % self._topic)
379 379
380 380 def _showitem(self):
381 381 self._out.write(
382 382 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
383 383 )
384 384
385 385 def end(self):
386 386 baseformatter.end(self)
387 387 self._out.write(b"]\n")
388 388
389 389
390 390 class pickleformatter(baseformatter):
391 391 def __init__(self, ui, out, topic, opts):
392 392 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
393 393 self._out = out
394 394 self._data = []
395 395
396 396 def _showitem(self):
397 397 self._data.append(self._item)
398 398
399 399 def end(self):
400 400 baseformatter.end(self)
401 401 self._out.write(pickle.dumps(self._data))
402 402
403 403
404 404 class cborformatter(baseformatter):
405 405 '''serialize items as an indefinite-length CBOR array'''
406 406
407 407 def __init__(self, ui, out, topic, opts):
408 408 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
409 409 self._out = out
410 410 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
411 411
412 412 def _showitem(self):
413 413 self._out.write(b''.join(cborutil.streamencode(self._item)))
414 414
415 415 def end(self):
416 416 baseformatter.end(self)
417 417 self._out.write(cborutil.BREAK)
418 418
419 419
420 420 class jsonformatter(baseformatter):
421 421 def __init__(self, ui, out, topic, opts):
422 422 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
423 423 self._out = out
424 424 self._out.write(b"[")
425 425 self._first = True
426 426
427 427 def _showitem(self):
428 428 if self._first:
429 429 self._first = False
430 430 else:
431 431 self._out.write(b",")
432 432
433 433 self._out.write(b"\n {\n")
434 434 first = True
435 435 for k, v in sorted(self._item.items()):
436 436 if first:
437 437 first = False
438 438 else:
439 439 self._out.write(b",\n")
440 440 u = templatefilters.json(v, paranoid=False)
441 441 self._out.write(b' "%s": %s' % (k, u))
442 442 self._out.write(b"\n }")
443 443
444 444 def end(self):
445 445 baseformatter.end(self)
446 446 self._out.write(b"\n]\n")
447 447
448 448
449 449 class _templateconverter(object):
450 450 '''convert non-primitive data types to be processed by templater'''
451 451
452 452 storecontext = True
453 453
454 454 @staticmethod
455 455 def wrapnested(data, tmpl, sep):
456 456 '''wrap nested data by templatable type'''
457 457 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
458 458
459 459 @staticmethod
460 460 def formatdate(date, fmt):
461 461 '''return date tuple'''
462 462 return templateutil.date(date)
463 463
464 464 @staticmethod
465 465 def formatdict(data, key, value, fmt, sep):
466 466 '''build object that can be evaluated as either plain string or dict'''
467 467 data = util.sortdict(_iteritems(data))
468 468
469 469 def f():
470 470 yield _plainconverter.formatdict(data, key, value, fmt, sep)
471 471
472 472 return templateutil.hybriddict(
473 473 data, key=key, value=value, fmt=fmt, gen=f
474 474 )
475 475
476 476 @staticmethod
477 477 def formatlist(data, name, fmt, sep):
478 478 '''build object that can be evaluated as either plain string or list'''
479 479 data = list(data)
480 480
481 481 def f():
482 482 yield _plainconverter.formatlist(data, name, fmt, sep)
483 483
484 484 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
485 485
486 486
487 487 class templateformatter(baseformatter):
488 488 def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
489 489 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
490 490 self._out = out
491 491 self._tref = spec.ref
492 492 self._t = loadtemplater(
493 493 ui,
494 494 spec,
495 495 defaults=templatekw.keywords,
496 496 resources=templateresources(ui),
497 497 cache=templatekw.defaulttempl,
498 498 )
499 499 if overridetemplates:
500 500 self._t.cache.update(overridetemplates)
501 501 self._parts = templatepartsmap(
502 502 spec, self._t, [b'docheader', b'docfooter', b'separator']
503 503 )
504 504 self._counter = itertools.count()
505 505 self._renderitem(b'docheader', {})
506 506
507 507 def _showitem(self):
508 508 item = self._item.copy()
509 509 item[b'index'] = index = next(self._counter)
510 510 if index > 0:
511 511 self._renderitem(b'separator', {})
512 512 self._renderitem(self._tref, item)
513 513
514 514 def _renderitem(self, part, item):
515 515 if part not in self._parts:
516 516 return
517 517 ref = self._parts[part]
518 # None can't be put in the mapping dict since it means <unset>
519 for k, v in item.items():
520 if v is None:
521 item[k] = templateutil.wrappedvalue(v)
518 522 self._out.write(self._t.render(ref, item))
519 523
520 524 @util.propertycache
521 525 def _symbolsused(self):
522 526 return self._t.symbolsused(self._tref)
523 527
524 528 def datahint(self):
525 529 '''set of field names to be referenced from the template'''
526 530 return self._symbolsused[0]
527 531
528 532 def end(self):
529 533 baseformatter.end(self)
530 534 self._renderitem(b'docfooter', {})
531 535
532 536
533 537 @attr.s(frozen=True)
534 538 class templatespec(object):
535 539 ref = attr.ib()
536 540 tmpl = attr.ib()
537 541 mapfile = attr.ib()
538 542 refargs = attr.ib(default=None)
539 543
540 544
541 545 def lookuptemplate(ui, topic, tmpl):
542 546 """Find the template matching the given -T/--template spec 'tmpl'
543 547
544 548 'tmpl' can be any of the following:
545 549
546 550 - a literal template (e.g. '{rev}')
547 551 - a reference to built-in template (i.e. formatter)
548 552 - a map-file name or path (e.g. 'changelog')
549 553 - a reference to [templates] in config file
550 554 - a path to raw template file
551 555
552 556 A map file defines a stand-alone template environment. If a map file
553 557 selected, all templates defined in the file will be loaded, and the
554 558 template matching the given topic will be rendered. Aliases won't be
555 559 loaded from user config, but from the map file.
556 560
557 561 If no map file selected, all templates in [templates] section will be
558 562 available as well as aliases in [templatealias].
559 563 """
560 564
561 565 if not tmpl:
562 566 return templatespec(None, None, None)
563 567
564 568 # looks like a literal template?
565 569 if b'{' in tmpl:
566 570 return templatespec(b'', tmpl, None)
567 571
568 572 # a reference to built-in (formatter) template
569 573 if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
570 574 return templatespec(tmpl, None, None)
571 575
572 576 # a function-style reference to built-in template
573 577 func, fsep, ftail = tmpl.partition(b'(')
574 578 if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
575 579 templater.parseexpr(tmpl) # make sure syntax errors are confined
576 580 return templatespec(func, None, None, refargs=ftail[:-1])
577 581
578 582 # perhaps a stock style?
579 583 if not os.path.split(tmpl)[0]:
580 584 mapname = templater.templatepath(
581 585 b'map-cmdline.' + tmpl
582 586 ) or templater.templatepath(tmpl)
583 587 if mapname and os.path.isfile(mapname):
584 588 return templatespec(topic, None, mapname)
585 589
586 590 # perhaps it's a reference to [templates]
587 591 if ui.config(b'templates', tmpl):
588 592 return templatespec(tmpl, None, None)
589 593
590 594 if tmpl == b'list':
591 595 ui.write(_(b"available styles: %s\n") % templater.stylelist())
592 596 raise error.Abort(_(b"specify a template"))
593 597
594 598 # perhaps it's a path to a map or a template
595 599 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
596 600 # is it a mapfile for a style?
597 601 if os.path.basename(tmpl).startswith(b"map-"):
598 602 return templatespec(topic, None, os.path.realpath(tmpl))
599 603 with util.posixfile(tmpl, b'rb') as f:
600 604 tmpl = f.read()
601 605 return templatespec(b'', tmpl, None)
602 606
603 607 # constant string?
604 608 return templatespec(b'', tmpl, None)
605 609
606 610
607 611 def templatepartsmap(spec, t, partnames):
608 612 """Create a mapping of {part: ref}"""
609 613 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
610 614 if spec.mapfile:
611 615 partsmap.update((p, p) for p in partnames if p in t)
612 616 elif spec.ref:
613 617 for part in partnames:
614 618 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
615 619 if ref in t:
616 620 partsmap[part] = ref
617 621 return partsmap
618 622
619 623
620 624 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
621 625 """Create a templater from either a literal template or loading from
622 626 a map file"""
623 627 assert not (spec.tmpl and spec.mapfile)
624 628 if spec.mapfile:
625 629 frommapfile = templater.templater.frommapfile
626 630 return frommapfile(
627 631 spec.mapfile, defaults=defaults, resources=resources, cache=cache
628 632 )
629 633 return maketemplater(
630 634 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
631 635 )
632 636
633 637
634 638 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
635 639 """Create a templater from a string template 'tmpl'"""
636 640 aliases = ui.configitems(b'templatealias')
637 641 t = templater.templater(
638 642 defaults=defaults, resources=resources, cache=cache, aliases=aliases
639 643 )
640 644 t.cache.update(
641 645 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
642 646 )
643 647 if tmpl:
644 648 t.cache[b''] = tmpl
645 649 return t
646 650
647 651
648 652 # marker to denote a resource to be loaded on demand based on mapping values
649 653 # (e.g. (ctx, path) -> fctx)
650 654 _placeholder = object()
651 655
652 656
653 657 class templateresources(templater.resourcemapper):
654 658 """Resource mapper designed for the default templatekw and function"""
655 659
656 660 def __init__(self, ui, repo=None):
657 661 self._resmap = {
658 662 b'cache': {}, # for templatekw/funcs to store reusable data
659 663 b'repo': repo,
660 664 b'ui': ui,
661 665 }
662 666
663 667 def availablekeys(self, mapping):
664 668 return {
665 669 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
666 670 }
667 671
668 672 def knownkeys(self):
669 673 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
670 674
671 675 def lookup(self, mapping, key):
672 676 if key not in self.knownkeys():
673 677 return None
674 678 v = self._getsome(mapping, key)
675 679 if v is _placeholder:
676 680 v = mapping[key] = self._loadermap[key](self, mapping)
677 681 return v
678 682
679 683 def populatemap(self, context, origmapping, newmapping):
680 684 mapping = {}
681 685 if self._hasnodespec(newmapping):
682 686 mapping[b'revcache'] = {} # per-ctx cache
683 687 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
684 688 orignode = templateutil.runsymbol(context, origmapping, b'node')
685 689 mapping[b'originalnode'] = orignode
686 690 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
687 691 # its existence to be reported by availablekeys()
688 692 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
689 693 mapping[b'ctx'] = _placeholder
690 694 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
691 695 mapping[b'fctx'] = _placeholder
692 696 return mapping
693 697
694 698 def _getsome(self, mapping, key):
695 699 v = mapping.get(key)
696 700 if v is not None:
697 701 return v
698 702 return self._resmap.get(key)
699 703
700 704 def _hasliteral(self, mapping, key):
701 705 """Test if a literal value is set or unset in the given mapping"""
702 706 return key in mapping and not callable(mapping[key])
703 707
704 708 def _getliteral(self, mapping, key):
705 709 """Return value of the given name if it is a literal"""
706 710 v = mapping.get(key)
707 711 if callable(v):
708 712 return None
709 713 return v
710 714
711 715 def _hasnodespec(self, mapping):
712 716 """Test if context revision is set or unset in the given mapping"""
713 717 return b'node' in mapping or b'ctx' in mapping
714 718
715 719 def _loadctx(self, mapping):
716 720 repo = self._getsome(mapping, b'repo')
717 721 node = self._getliteral(mapping, b'node')
718 722 if repo is None or node is None:
719 723 return
720 724 try:
721 725 return repo[node]
722 726 except error.RepoLookupError:
723 727 return None # maybe hidden/non-existent node
724 728
725 729 def _loadfctx(self, mapping):
726 730 ctx = self._getsome(mapping, b'ctx')
727 731 path = self._getliteral(mapping, b'path')
728 732 if ctx is None or path is None:
729 733 return None
730 734 try:
731 735 return ctx[path]
732 736 except error.LookupError:
733 737 return None # maybe removed file?
734 738
735 739 _loadermap = {
736 740 b'ctx': _loadctx,
737 741 b'fctx': _loadfctx,
738 742 }
739 743
740 744
741 745 def _internaltemplateformatter(
742 746 ui,
743 747 out,
744 748 topic,
745 749 opts,
746 750 spec,
747 751 tmpl,
748 752 docheader=b'',
749 753 docfooter=b'',
750 754 separator=b'',
751 755 ):
752 756 """Build template formatter that handles customizable built-in templates
753 757 such as -Tjson(...)"""
754 758 templates = {spec.ref: tmpl}
755 759 if docheader:
756 760 templates[b'%s:docheader' % spec.ref] = docheader
757 761 if docfooter:
758 762 templates[b'%s:docfooter' % spec.ref] = docfooter
759 763 if separator:
760 764 templates[b'%s:separator' % spec.ref] = separator
761 765 return templateformatter(
762 766 ui, out, topic, opts, spec, overridetemplates=templates
763 767 )
764 768
765 769
766 770 def formatter(ui, out, topic, opts):
767 771 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
768 772 if spec.ref == b"cbor" and spec.refargs is not None:
769 773 return _internaltemplateformatter(
770 774 ui,
771 775 out,
772 776 topic,
773 777 opts,
774 778 spec,
775 779 tmpl=b'{dict(%s)|cbor}' % spec.refargs,
776 780 docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
777 781 docfooter=cborutil.BREAK,
778 782 )
779 783 elif spec.ref == b"cbor":
780 784 return cborformatter(ui, out, topic, opts)
781 785 elif spec.ref == b"json" and spec.refargs is not None:
782 786 return _internaltemplateformatter(
783 787 ui,
784 788 out,
785 789 topic,
786 790 opts,
787 791 spec,
788 792 tmpl=b'{dict(%s)|json}' % spec.refargs,
789 793 docheader=b'[\n ',
790 794 docfooter=b'\n]\n',
791 795 separator=b',\n ',
792 796 )
793 797 elif spec.ref == b"json":
794 798 return jsonformatter(ui, out, topic, opts)
795 799 elif spec.ref == b"pickle":
796 800 assert spec.refargs is None, r'function-style not supported'
797 801 return pickleformatter(ui, out, topic, opts)
798 802 elif spec.ref == b"debug":
799 803 assert spec.refargs is None, r'function-style not supported'
800 804 return debugformatter(ui, out, topic, opts)
801 805 elif spec.ref or spec.tmpl or spec.mapfile:
802 806 assert spec.refargs is None, r'function-style not supported'
803 807 return templateformatter(ui, out, topic, opts, spec)
804 808 # developer config: ui.formatdebug
805 809 elif ui.configbool(b'ui', b'formatdebug'):
806 810 return debugformatter(ui, out, topic, opts)
807 811 # deprecated config: ui.formatjson
808 812 elif ui.configbool(b'ui', b'formatjson'):
809 813 return jsonformatter(ui, out, topic, opts)
810 814 return plainformatter(ui, out, topic, opts)
811 815
812 816
813 817 @contextlib.contextmanager
814 818 def openformatter(ui, filename, topic, opts):
815 819 """Create a formatter that writes outputs to the specified file
816 820
817 821 Must be invoked using the 'with' statement.
818 822 """
819 823 with util.posixfile(filename, b'wb') as out:
820 824 with formatter(ui, out, topic, opts) as fm:
821 825 yield fm
822 826
823 827
824 828 @contextlib.contextmanager
825 829 def _neverending(fm):
826 830 yield fm
827 831
828 832
829 833 def maybereopen(fm, filename):
830 834 """Create a formatter backed by file if filename specified, else return
831 835 the given formatter
832 836
833 837 Must be invoked using the 'with' statement. This will never call fm.end()
834 838 of the given formatter.
835 839 """
836 840 if filename:
837 841 return openformatter(fm._ui, filename, fm._topic, fm._opts)
838 842 else:
839 843 return _neverending(fm)
@@ -1,392 +1,392 b''
1 1 hide outer repo
2 2 $ hg init
3 3
4 4 Invalid syntax: no value
5 5
6 6 $ cat > .hg/hgrc << EOF
7 7 > novaluekey
8 8 > EOF
9 9 $ hg showconfig
10 10 hg: parse error at $TESTTMP/.hg/hgrc:1: novaluekey
11 11 [255]
12 12
13 13 Invalid syntax: no key
14 14
15 15 $ cat > .hg/hgrc << EOF
16 16 > =nokeyvalue
17 17 > EOF
18 18 $ hg showconfig
19 19 hg: parse error at $TESTTMP/.hg/hgrc:1: =nokeyvalue
20 20 [255]
21 21
22 22 Test hint about invalid syntax from leading white space
23 23
24 24 $ cat > .hg/hgrc << EOF
25 25 > key=value
26 26 > EOF
27 27 $ hg showconfig
28 28 hg: parse error at $TESTTMP/.hg/hgrc:1: key=value
29 29 unexpected leading whitespace
30 30 [255]
31 31
32 32 $ cat > .hg/hgrc << EOF
33 33 > [section]
34 34 > key=value
35 35 > EOF
36 36 $ hg showconfig
37 37 hg: parse error at $TESTTMP/.hg/hgrc:1: [section]
38 38 unexpected leading whitespace
39 39 [255]
40 40
41 41 Reset hgrc
42 42
43 43 $ echo > .hg/hgrc
44 44
45 45 Test case sensitive configuration
46 46
47 47 $ cat <<EOF >> $HGRCPATH
48 48 > [Section]
49 49 > KeY = Case Sensitive
50 50 > key = lower case
51 51 > EOF
52 52
53 53 $ hg showconfig Section
54 54 Section.KeY=Case Sensitive
55 55 Section.key=lower case
56 56
57 57 $ hg showconfig Section -Tjson
58 58 [
59 59 {
60 60 "defaultvalue": null,
61 61 "name": "Section.KeY",
62 62 "source": "*.hgrc:*", (glob)
63 63 "value": "Case Sensitive"
64 64 },
65 65 {
66 66 "defaultvalue": null,
67 67 "name": "Section.key",
68 68 "source": "*.hgrc:*", (glob)
69 69 "value": "lower case"
70 70 }
71 71 ]
72 72 $ hg showconfig Section.KeY -Tjson
73 73 [
74 74 {
75 75 "defaultvalue": null,
76 76 "name": "Section.KeY",
77 77 "source": "*.hgrc:*", (glob)
78 78 "value": "Case Sensitive"
79 79 }
80 80 ]
81 81 $ hg showconfig -Tjson | tail -7
82 82 {
83 83 "defaultvalue": null,
84 84 "name": "*", (glob)
85 85 "source": "*", (glob)
86 86 "value": "*" (glob)
87 87 }
88 88 ]
89 89
90 90 Test config default of various types:
91 91
92 92 {"defaultvalue": ""} for -T'json(defaultvalue)' looks weird, but that's
93 93 how the templater works. Unknown keywords are evaluated to "".
94 94
95 95 dynamicdefault
96 96
97 97 $ hg config --config alias.foo= alias -Tjson
98 98 [
99 99 {
100 100 "name": "alias.foo",
101 101 "source": "--config",
102 102 "value": ""
103 103 }
104 104 ]
105 105 $ hg config --config alias.foo= alias -T'json(defaultvalue)'
106 106 [
107 107 {"defaultvalue": ""}
108 108 ]
109 109 $ hg config --config alias.foo= alias -T'{defaultvalue}\n'
110 110
111 111
112 112 null
113 113
114 114 $ hg config --config auth.cookiefile= auth -Tjson
115 115 [
116 116 {
117 117 "defaultvalue": null,
118 118 "name": "auth.cookiefile",
119 119 "source": "--config",
120 120 "value": ""
121 121 }
122 122 ]
123 123 $ hg config --config auth.cookiefile= auth -T'json(defaultvalue)'
124 124 [
125 {"defaultvalue": ""}
125 {"defaultvalue": null}
126 126 ]
127 127 $ hg config --config auth.cookiefile= auth -T'{defaultvalue}\n'
128 128
129 129
130 130 false
131 131
132 132 $ hg config --config commands.commit.post-status= commands -Tjson
133 133 [
134 134 {
135 135 "defaultvalue": false,
136 136 "name": "commands.commit.post-status",
137 137 "source": "--config",
138 138 "value": ""
139 139 }
140 140 ]
141 141 $ hg config --config commands.commit.post-status= commands -T'json(defaultvalue)'
142 142 [
143 143 {"defaultvalue": false}
144 144 ]
145 145 $ hg config --config commands.commit.post-status= commands -T'{defaultvalue}\n'
146 146 False
147 147
148 148 true
149 149
150 150 $ hg config --config format.dotencode= format -Tjson
151 151 [
152 152 {
153 153 "defaultvalue": true,
154 154 "name": "format.dotencode",
155 155 "source": "--config",
156 156 "value": ""
157 157 }
158 158 ]
159 159 $ hg config --config format.dotencode= format -T'json(defaultvalue)'
160 160 [
161 161 {"defaultvalue": true}
162 162 ]
163 163 $ hg config --config format.dotencode= format -T'{defaultvalue}\n'
164 164 True
165 165
166 166 bytes
167 167
168 168 $ hg config --config commands.resolve.mark-check= commands -Tjson
169 169 [
170 170 {
171 171 "defaultvalue": "none",
172 172 "name": "commands.resolve.mark-check",
173 173 "source": "--config",
174 174 "value": ""
175 175 }
176 176 ]
177 177 $ hg config --config commands.resolve.mark-check= commands -T'json(defaultvalue)'
178 178 [
179 179 {"defaultvalue": "none"}
180 180 ]
181 181 $ hg config --config commands.resolve.mark-check= commands -T'{defaultvalue}\n'
182 182 none
183 183
184 184 empty list
185 185
186 186 $ hg config --config commands.show.aliasprefix= commands -Tjson
187 187 [
188 188 {
189 189 "defaultvalue": [],
190 190 "name": "commands.show.aliasprefix",
191 191 "source": "--config",
192 192 "value": ""
193 193 }
194 194 ]
195 195 $ hg config --config commands.show.aliasprefix= commands -T'json(defaultvalue)'
196 196 [
197 197 {"defaultvalue": []}
198 198 ]
199 199 $ hg config --config commands.show.aliasprefix= commands -T'{defaultvalue}\n'
200 200
201 201
202 202 nonempty list
203 203
204 204 $ hg config --config progress.format= progress -Tjson
205 205 [
206 206 {
207 207 "defaultvalue": ["topic", "bar", "number", "estimate"],
208 208 "name": "progress.format",
209 209 "source": "--config",
210 210 "value": ""
211 211 }
212 212 ]
213 213 $ hg config --config progress.format= progress -T'json(defaultvalue)'
214 214 [
215 215 {"defaultvalue": ["topic", "bar", "number", "estimate"]}
216 216 ]
217 217 $ hg config --config progress.format= progress -T'{defaultvalue}\n'
218 218 topic bar number estimate
219 219
220 220 int
221 221
222 222 $ hg config --config profiling.freq= profiling -Tjson
223 223 [
224 224 {
225 225 "defaultvalue": 1000,
226 226 "name": "profiling.freq",
227 227 "source": "--config",
228 228 "value": ""
229 229 }
230 230 ]
231 231 $ hg config --config profiling.freq= profiling -T'json(defaultvalue)'
232 232 [
233 233 {"defaultvalue": 1000}
234 234 ]
235 235 $ hg config --config profiling.freq= profiling -T'{defaultvalue}\n'
236 236 1000
237 237
238 238 float
239 239
240 240 $ hg config --config profiling.showmax= profiling -Tjson
241 241 [
242 242 {
243 243 "defaultvalue": 0.999,
244 244 "name": "profiling.showmax",
245 245 "source": "--config",
246 246 "value": ""
247 247 }
248 248 ]
249 249 $ hg config --config profiling.showmax= profiling -T'json(defaultvalue)'
250 250 [
251 251 {"defaultvalue": 0.999}
252 252 ]
253 253 $ hg config --config profiling.showmax= profiling -T'{defaultvalue}\n'
254 254 0.999
255 255
256 256 Test empty config source:
257 257
258 258 $ cat <<EOF > emptysource.py
259 259 > def reposetup(ui, repo):
260 260 > ui.setconfig(b'empty', b'source', b'value')
261 261 > EOF
262 262 $ cp .hg/hgrc .hg/hgrc.orig
263 263 $ cat <<EOF >> .hg/hgrc
264 264 > [extensions]
265 265 > emptysource = `pwd`/emptysource.py
266 266 > EOF
267 267
268 268 $ hg config --debug empty.source
269 269 read config from: * (glob)
270 270 none: value
271 271 $ hg config empty.source -Tjson
272 272 [
273 273 {
274 274 "defaultvalue": null,
275 275 "name": "empty.source",
276 276 "source": "",
277 277 "value": "value"
278 278 }
279 279 ]
280 280
281 281 $ cp .hg/hgrc.orig .hg/hgrc
282 282
283 283 Test "%unset"
284 284
285 285 $ cat >> $HGRCPATH <<EOF
286 286 > [unsettest]
287 287 > local-hgrcpath = should be unset (HGRCPATH)
288 288 > %unset local-hgrcpath
289 289 >
290 290 > global = should be unset (HGRCPATH)
291 291 >
292 292 > both = should be unset (HGRCPATH)
293 293 >
294 294 > set-after-unset = should be unset (HGRCPATH)
295 295 > EOF
296 296
297 297 $ cat >> .hg/hgrc <<EOF
298 298 > [unsettest]
299 299 > local-hgrc = should be unset (.hg/hgrc)
300 300 > %unset local-hgrc
301 301 >
302 302 > %unset global
303 303 >
304 304 > both = should be unset (.hg/hgrc)
305 305 > %unset both
306 306 >
307 307 > set-after-unset = should be unset (.hg/hgrc)
308 308 > %unset set-after-unset
309 309 > set-after-unset = should be set (.hg/hgrc)
310 310 > EOF
311 311
312 312 $ hg showconfig unsettest
313 313 unsettest.set-after-unset=should be set (.hg/hgrc)
314 314
315 315 Test exit code when no config matches
316 316
317 317 $ hg config Section.idontexist
318 318 [1]
319 319
320 320 sub-options in [paths] aren't expanded
321 321
322 322 $ cat > .hg/hgrc << EOF
323 323 > [paths]
324 324 > foo = ~/foo
325 325 > foo:suboption = ~/foo
326 326 > EOF
327 327
328 328 $ hg showconfig paths
329 329 paths.foo:suboption=~/foo
330 330 paths.foo=$TESTTMP/foo
331 331
332 332 edit failure
333 333
334 334 $ HGEDITOR=false hg config --edit
335 335 abort: edit failed: false exited with status 1
336 336 [255]
337 337
338 338 config affected by environment variables
339 339
340 340 $ EDITOR=e1 VISUAL=e2 hg config --debug | grep 'ui\.editor'
341 341 $VISUAL: ui.editor=e2
342 342
343 343 $ VISUAL=e2 hg config --debug --config ui.editor=e3 | grep 'ui\.editor'
344 344 --config: ui.editor=e3
345 345
346 346 $ PAGER=p1 hg config --debug | grep 'pager\.pager'
347 347 $PAGER: pager.pager=p1
348 348
349 349 $ PAGER=p1 hg config --debug --config pager.pager=p2 | grep 'pager\.pager'
350 350 --config: pager.pager=p2
351 351
352 352 verify that aliases are evaluated as well
353 353
354 354 $ hg init aliastest
355 355 $ cd aliastest
356 356 $ cat > .hg/hgrc << EOF
357 357 > [ui]
358 358 > user = repo user
359 359 > EOF
360 360 $ touch index
361 361 $ unset HGUSER
362 362 $ hg ci -Am test
363 363 adding index
364 364 $ hg log --template '{author}\n'
365 365 repo user
366 366 $ cd ..
367 367
368 368 alias has lower priority
369 369
370 370 $ hg init aliaspriority
371 371 $ cd aliaspriority
372 372 $ cat > .hg/hgrc << EOF
373 373 > [ui]
374 374 > user = alias user
375 375 > username = repo user
376 376 > EOF
377 377 $ touch index
378 378 $ unset HGUSER
379 379 $ hg ci -Am test
380 380 adding index
381 381 $ hg log --template '{author}\n'
382 382 repo user
383 383 $ cd ..
384 384
385 385 configs should be read in lexicographical order
386 386
387 387 $ mkdir configs
388 388 $ for i in `$TESTDIR/seq.py 10 99`; do
389 389 > printf "[section]\nkey=$i" > configs/$i.rc
390 390 > done
391 391 $ HGRCPATH=configs hg config section.key
392 392 99
General Comments 0
You need to be logged in to leave comments. Login now