##// END OF EJS Templates
formatter: port handling of 'originalnode' to populatemap() hook...
Yuya Nishihara -
r37122:7db3c28d default
parent child Browse files
Show More
@@ -1,594 +1,596 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'))
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 collections
111 111 import contextlib
112 112 import itertools
113 113 import os
114 114
115 115 from .i18n import _
116 116 from .node import (
117 117 hex,
118 118 short,
119 119 )
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 dateutil
131 131
132 132 pickle = util.pickle
133 133
134 134 class _nullconverter(object):
135 135 '''convert non-primitive data types to be processed by formatter'''
136 136
137 137 # set to True if context object should be stored as item
138 138 storecontext = False
139 139
140 140 @staticmethod
141 141 def formatdate(date, fmt):
142 142 '''convert date tuple to appropriate format'''
143 143 return date
144 144 @staticmethod
145 145 def formatdict(data, key, value, fmt, sep):
146 146 '''convert dict or key-value pairs to appropriate dict format'''
147 147 # use plain dict instead of util.sortdict so that data can be
148 148 # serialized as a builtin dict in pickle output
149 149 return dict(data)
150 150 @staticmethod
151 151 def formatlist(data, name, fmt, sep):
152 152 '''convert iterable to appropriate list format'''
153 153 return list(data)
154 154
155 155 class baseformatter(object):
156 156 def __init__(self, ui, topic, opts, converter):
157 157 self._ui = ui
158 158 self._topic = topic
159 159 self._style = opts.get("style")
160 160 self._template = opts.get("template")
161 161 self._converter = converter
162 162 self._item = None
163 163 # function to convert node to string suitable for this output
164 164 self.hexfunc = hex
165 165 def __enter__(self):
166 166 return self
167 167 def __exit__(self, exctype, excvalue, traceback):
168 168 if exctype is None:
169 169 self.end()
170 170 def _showitem(self):
171 171 '''show a formatted item once all data is collected'''
172 172 def startitem(self):
173 173 '''begin an item in the format list'''
174 174 if self._item is not None:
175 175 self._showitem()
176 176 self._item = {}
177 177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
178 178 '''convert date tuple to appropriate format'''
179 179 return self._converter.formatdate(date, fmt)
180 180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
181 181 '''convert dict or key-value pairs to appropriate dict format'''
182 182 return self._converter.formatdict(data, key, value, fmt, sep)
183 183 def formatlist(self, data, name, fmt=None, sep=' '):
184 184 '''convert iterable to appropriate list format'''
185 185 # name is mandatory argument for now, but it could be optional if
186 186 # we have default template keyword, e.g. {item}
187 187 return self._converter.formatlist(data, name, fmt, sep)
188 188 def context(self, **ctxs):
189 189 '''insert context objects to be used to render template keywords'''
190 190 ctxs = pycompat.byteskwargs(ctxs)
191 191 assert all(k in {'ctx', 'fctx'} for k in ctxs)
192 192 if self._converter.storecontext:
193 193 self._item.update(ctxs)
194 194 def data(self, **data):
195 195 '''insert data into item that's not shown in default output'''
196 196 data = pycompat.byteskwargs(data)
197 197 self._item.update(data)
198 198 def write(self, fields, deftext, *fielddata, **opts):
199 199 '''do default text output while assigning data to item'''
200 200 fieldkeys = fields.split()
201 201 assert len(fieldkeys) == len(fielddata)
202 202 self._item.update(zip(fieldkeys, fielddata))
203 203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
204 204 '''do conditional write (primarily for plain formatter)'''
205 205 fieldkeys = fields.split()
206 206 assert len(fieldkeys) == len(fielddata)
207 207 self._item.update(zip(fieldkeys, fielddata))
208 208 def plain(self, text, **opts):
209 209 '''show raw text for non-templated mode'''
210 210 def isplain(self):
211 211 '''check for plain formatter usage'''
212 212 return False
213 213 def nested(self, field):
214 214 '''sub formatter to store nested data in the specified field'''
215 215 self._item[field] = data = []
216 216 return _nestedformatter(self._ui, self._converter, data)
217 217 def end(self):
218 218 '''end output for the formatter'''
219 219 if self._item is not None:
220 220 self._showitem()
221 221
222 222 def nullformatter(ui, topic):
223 223 '''formatter that prints nothing'''
224 224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
225 225
226 226 class _nestedformatter(baseformatter):
227 227 '''build sub items and store them in the parent formatter'''
228 228 def __init__(self, ui, converter, data):
229 229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
230 230 self._data = data
231 231 def _showitem(self):
232 232 self._data.append(self._item)
233 233
234 234 def _iteritems(data):
235 235 '''iterate key-value pairs in stable order'''
236 236 if isinstance(data, dict):
237 237 return sorted(data.iteritems())
238 238 return data
239 239
240 240 class _plainconverter(object):
241 241 '''convert non-primitive data types to text'''
242 242
243 243 storecontext = False
244 244
245 245 @staticmethod
246 246 def formatdate(date, fmt):
247 247 '''stringify date tuple in the given format'''
248 248 return dateutil.datestr(date, fmt)
249 249 @staticmethod
250 250 def formatdict(data, key, value, fmt, sep):
251 251 '''stringify key-value pairs separated by sep'''
252 252 prefmt = pycompat.identity
253 253 if fmt is None:
254 254 fmt = '%s=%s'
255 255 prefmt = pycompat.bytestr
256 256 return sep.join(fmt % (prefmt(k), prefmt(v))
257 257 for k, v in _iteritems(data))
258 258 @staticmethod
259 259 def formatlist(data, name, fmt, sep):
260 260 '''stringify iterable separated by sep'''
261 261 prefmt = pycompat.identity
262 262 if fmt is None:
263 263 fmt = '%s'
264 264 prefmt = pycompat.bytestr
265 265 return sep.join(fmt % prefmt(e) for e in data)
266 266
267 267 class plainformatter(baseformatter):
268 268 '''the default text output scheme'''
269 269 def __init__(self, ui, out, topic, opts):
270 270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
271 271 if ui.debugflag:
272 272 self.hexfunc = hex
273 273 else:
274 274 self.hexfunc = short
275 275 if ui is out:
276 276 self._write = ui.write
277 277 else:
278 278 self._write = lambda s, **opts: out.write(s)
279 279 def startitem(self):
280 280 pass
281 281 def data(self, **data):
282 282 pass
283 283 def write(self, fields, deftext, *fielddata, **opts):
284 284 self._write(deftext % fielddata, **opts)
285 285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
286 286 '''do conditional write'''
287 287 if cond:
288 288 self._write(deftext % fielddata, **opts)
289 289 def plain(self, text, **opts):
290 290 self._write(text, **opts)
291 291 def isplain(self):
292 292 return True
293 293 def nested(self, field):
294 294 # nested data will be directly written to ui
295 295 return self
296 296 def end(self):
297 297 pass
298 298
299 299 class debugformatter(baseformatter):
300 300 def __init__(self, ui, out, topic, opts):
301 301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 302 self._out = out
303 303 self._out.write("%s = [\n" % self._topic)
304 304 def _showitem(self):
305 305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
306 306 def end(self):
307 307 baseformatter.end(self)
308 308 self._out.write("]\n")
309 309
310 310 class pickleformatter(baseformatter):
311 311 def __init__(self, ui, out, topic, opts):
312 312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 313 self._out = out
314 314 self._data = []
315 315 def _showitem(self):
316 316 self._data.append(self._item)
317 317 def end(self):
318 318 baseformatter.end(self)
319 319 self._out.write(pickle.dumps(self._data))
320 320
321 321 class jsonformatter(baseformatter):
322 322 def __init__(self, ui, out, topic, opts):
323 323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
324 324 self._out = out
325 325 self._out.write("[")
326 326 self._first = True
327 327 def _showitem(self):
328 328 if self._first:
329 329 self._first = False
330 330 else:
331 331 self._out.write(",")
332 332
333 333 self._out.write("\n {\n")
334 334 first = True
335 335 for k, v in sorted(self._item.items()):
336 336 if first:
337 337 first = False
338 338 else:
339 339 self._out.write(",\n")
340 340 u = templatefilters.json(v, paranoid=False)
341 341 self._out.write(' "%s": %s' % (k, u))
342 342 self._out.write("\n }")
343 343 def end(self):
344 344 baseformatter.end(self)
345 345 self._out.write("\n]\n")
346 346
347 347 class _templateconverter(object):
348 348 '''convert non-primitive data types to be processed by templater'''
349 349
350 350 storecontext = True
351 351
352 352 @staticmethod
353 353 def formatdate(date, fmt):
354 354 '''return date tuple'''
355 355 return date
356 356 @staticmethod
357 357 def formatdict(data, key, value, fmt, sep):
358 358 '''build object that can be evaluated as either plain string or dict'''
359 359 data = util.sortdict(_iteritems(data))
360 360 def f():
361 361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
362 362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
363 363 gen=f)
364 364 @staticmethod
365 365 def formatlist(data, name, fmt, sep):
366 366 '''build object that can be evaluated as either plain string or list'''
367 367 data = list(data)
368 368 def f():
369 369 yield _plainconverter.formatlist(data, name, fmt, sep)
370 370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
371 371
372 372 class templateformatter(baseformatter):
373 373 def __init__(self, ui, out, topic, opts):
374 374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
375 375 self._out = out
376 376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
377 377 self._tref = spec.ref
378 378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
379 379 resources=templateresources(ui),
380 380 cache=templatekw.defaulttempl)
381 381 self._parts = templatepartsmap(spec, self._t,
382 382 ['docheader', 'docfooter', 'separator'])
383 383 self._counter = itertools.count()
384 384 self._renderitem('docheader', {})
385 385
386 386 def _showitem(self):
387 387 item = self._item.copy()
388 388 item['index'] = index = next(self._counter)
389 389 if index > 0:
390 390 self._renderitem('separator', {})
391 391 self._renderitem(self._tref, item)
392 392
393 393 def _renderitem(self, part, item):
394 394 if part not in self._parts:
395 395 return
396 396 ref = self._parts[part]
397 397 self._out.write(self._t.render(ref, item))
398 398
399 399 def end(self):
400 400 baseformatter.end(self)
401 401 self._renderitem('docfooter', {})
402 402
403 403 templatespec = collections.namedtuple(r'templatespec',
404 404 r'ref tmpl mapfile')
405 405
406 406 def lookuptemplate(ui, topic, tmpl):
407 407 """Find the template matching the given -T/--template spec 'tmpl'
408 408
409 409 'tmpl' can be any of the following:
410 410
411 411 - a literal template (e.g. '{rev}')
412 412 - a map-file name or path (e.g. 'changelog')
413 413 - a reference to [templates] in config file
414 414 - a path to raw template file
415 415
416 416 A map file defines a stand-alone template environment. If a map file
417 417 selected, all templates defined in the file will be loaded, and the
418 418 template matching the given topic will be rendered. Aliases won't be
419 419 loaded from user config, but from the map file.
420 420
421 421 If no map file selected, all templates in [templates] section will be
422 422 available as well as aliases in [templatealias].
423 423 """
424 424
425 425 # looks like a literal template?
426 426 if '{' in tmpl:
427 427 return templatespec('', tmpl, None)
428 428
429 429 # perhaps a stock style?
430 430 if not os.path.split(tmpl)[0]:
431 431 mapname = (templater.templatepath('map-cmdline.' + tmpl)
432 432 or templater.templatepath(tmpl))
433 433 if mapname and os.path.isfile(mapname):
434 434 return templatespec(topic, None, mapname)
435 435
436 436 # perhaps it's a reference to [templates]
437 437 if ui.config('templates', tmpl):
438 438 return templatespec(tmpl, None, None)
439 439
440 440 if tmpl == 'list':
441 441 ui.write(_("available styles: %s\n") % templater.stylelist())
442 442 raise error.Abort(_("specify a template"))
443 443
444 444 # perhaps it's a path to a map or a template
445 445 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
446 446 # is it a mapfile for a style?
447 447 if os.path.basename(tmpl).startswith("map-"):
448 448 return templatespec(topic, None, os.path.realpath(tmpl))
449 449 with util.posixfile(tmpl, 'rb') as f:
450 450 tmpl = f.read()
451 451 return templatespec('', tmpl, None)
452 452
453 453 # constant string?
454 454 return templatespec('', tmpl, None)
455 455
456 456 def templatepartsmap(spec, t, partnames):
457 457 """Create a mapping of {part: ref}"""
458 458 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
459 459 if spec.mapfile:
460 460 partsmap.update((p, p) for p in partnames if p in t)
461 461 elif spec.ref:
462 462 for part in partnames:
463 463 ref = '%s:%s' % (spec.ref, part) # select config sub-section
464 464 if ref in t:
465 465 partsmap[part] = ref
466 466 return partsmap
467 467
468 468 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
469 469 """Create a templater from either a literal template or loading from
470 470 a map file"""
471 471 assert not (spec.tmpl and spec.mapfile)
472 472 if spec.mapfile:
473 473 frommapfile = templater.templater.frommapfile
474 474 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
475 475 cache=cache)
476 476 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
477 477 cache=cache)
478 478
479 479 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
480 480 """Create a templater from a string template 'tmpl'"""
481 481 aliases = ui.configitems('templatealias')
482 482 t = templater.templater(defaults=defaults, resources=resources,
483 483 cache=cache, aliases=aliases)
484 484 t.cache.update((k, templater.unquotestring(v))
485 485 for k, v in ui.configitems('templates'))
486 486 if tmpl:
487 487 t.cache[''] = tmpl
488 488 return t
489 489
490 490 class templateresources(templater.resourcemapper):
491 491 """Resource mapper designed for the default templatekw and function"""
492 492
493 493 def __init__(self, ui, repo=None):
494 494 self._resmap = {
495 495 'cache': {}, # for templatekw/funcs to store reusable data
496 496 'repo': repo,
497 497 'ui': ui,
498 498 }
499 499
500 500 def availablekeys(self, context, mapping):
501 501 return {k for k, g in self._gettermap.iteritems()
502 502 if g(self, context, mapping, k) is not None}
503 503
504 504 def knownkeys(self):
505 505 return self._knownkeys
506 506
507 507 def lookup(self, context, mapping, key):
508 508 get = self._gettermap.get(key)
509 509 if not get:
510 510 return None
511 511 return get(self, context, mapping, key)
512 512
513 513 def populatemap(self, context, origmapping, newmapping):
514 514 mapping = {}
515 515 if self._hasctx(newmapping):
516 516 mapping['revcache'] = {} # per-ctx cache
517 if 'node' in origmapping and 'node' in newmapping:
518 mapping['originalnode'] = origmapping['node']
517 519 return mapping
518 520
519 521 def _getsome(self, context, mapping, key):
520 522 v = mapping.get(key)
521 523 if v is not None:
522 524 return v
523 525 return self._resmap.get(key)
524 526
525 527 def _hasctx(self, mapping):
526 528 return 'ctx' in mapping or 'fctx' in mapping
527 529
528 530 def _getctx(self, context, mapping, key):
529 531 ctx = mapping.get('ctx')
530 532 if ctx is not None:
531 533 return ctx
532 534 fctx = mapping.get('fctx')
533 535 if fctx is not None:
534 536 return fctx.changectx()
535 537
536 538 def _getrepo(self, context, mapping, key):
537 539 ctx = self._getctx(context, mapping, 'ctx')
538 540 if ctx is not None:
539 541 return ctx.repo()
540 542 return self._getsome(context, mapping, key)
541 543
542 544 _gettermap = {
543 545 'cache': _getsome,
544 546 'ctx': _getctx,
545 547 'fctx': _getsome,
546 548 'repo': _getrepo,
547 549 'revcache': _getsome,
548 550 'ui': _getsome,
549 551 }
550 552 _knownkeys = set(_gettermap.keys())
551 553
552 554 def formatter(ui, out, topic, opts):
553 555 template = opts.get("template", "")
554 556 if template == "json":
555 557 return jsonformatter(ui, out, topic, opts)
556 558 elif template == "pickle":
557 559 return pickleformatter(ui, out, topic, opts)
558 560 elif template == "debug":
559 561 return debugformatter(ui, out, topic, opts)
560 562 elif template != "":
561 563 return templateformatter(ui, out, topic, opts)
562 564 # developer config: ui.formatdebug
563 565 elif ui.configbool('ui', 'formatdebug'):
564 566 return debugformatter(ui, out, topic, opts)
565 567 # deprecated config: ui.formatjson
566 568 elif ui.configbool('ui', 'formatjson'):
567 569 return jsonformatter(ui, out, topic, opts)
568 570 return plainformatter(ui, out, topic, opts)
569 571
570 572 @contextlib.contextmanager
571 573 def openformatter(ui, filename, topic, opts):
572 574 """Create a formatter that writes outputs to the specified file
573 575
574 576 Must be invoked using the 'with' statement.
575 577 """
576 578 with util.posixfile(filename, 'wb') as out:
577 579 with formatter(ui, out, topic, opts) as fm:
578 580 yield fm
579 581
580 582 @contextlib.contextmanager
581 583 def _neverending(fm):
582 584 yield fm
583 585
584 586 def maybereopen(fm, filename, opts):
585 587 """Create a formatter backed by file if filename specified, else return
586 588 the given formatter
587 589
588 590 Must be invoked using the 'with' statement. This will never call fm.end()
589 591 of the given formatter.
590 592 """
591 593 if filename:
592 594 return openformatter(fm._ui, filename, fm._topic, opts)
593 595 else:
594 596 return _neverending(fm)
@@ -1,452 +1,451 b''
1 1 # templateutil.py - utility for template evaluation
2 2 #
3 3 # Copyright 2005, 2006 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 types
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 )
18 18 from .utils import (
19 19 stringutil,
20 20 )
21 21
22 22 class ResourceUnavailable(error.Abort):
23 23 pass
24 24
25 25 class TemplateNotFound(error.Abort):
26 26 pass
27 27
28 28 class hybrid(object):
29 29 """Wrapper for list or dict to support legacy template
30 30
31 31 This class allows us to handle both:
32 32 - "{files}" (legacy command-line-specific list hack) and
33 33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 34 and to access raw values:
35 35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 36 - "{get(extras, key)}"
37 37 - "{files|json}"
38 38 """
39 39
40 40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 41 if gen is not None:
42 42 self.gen = gen # generator or function returning generator
43 43 self._values = values
44 44 self._makemap = makemap
45 45 self.joinfmt = joinfmt
46 46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 47 def gen(self):
48 48 """Default generator to stringify this as {join(self, ' ')}"""
49 49 for i, x in enumerate(self._values):
50 50 if i > 0:
51 51 yield ' '
52 52 yield self.joinfmt(x)
53 53 def itermaps(self):
54 54 makemap = self._makemap
55 55 for x in self._values:
56 56 yield makemap(x)
57 57 def __contains__(self, x):
58 58 return x in self._values
59 59 def __getitem__(self, key):
60 60 return self._values[key]
61 61 def __len__(self):
62 62 return len(self._values)
63 63 def __iter__(self):
64 64 return iter(self._values)
65 65 def __getattr__(self, name):
66 66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 67 r'itervalues', r'keys', r'values'):
68 68 raise AttributeError(name)
69 69 return getattr(self._values, name)
70 70
71 71 class mappable(object):
72 72 """Wrapper for non-list/dict object to support map operation
73 73
74 74 This class allows us to handle both:
75 75 - "{manifest}"
76 76 - "{manifest % '{rev}:{node}'}"
77 77 - "{manifest.rev}"
78 78
79 79 Unlike a hybrid, this does not simulate the behavior of the underling
80 80 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 81 """
82 82
83 83 def __init__(self, gen, key, value, makemap):
84 84 if gen is not None:
85 85 self.gen = gen # generator or function returning generator
86 86 self._key = key
87 87 self._value = value # may be generator of strings
88 88 self._makemap = makemap
89 89
90 90 def gen(self):
91 91 yield pycompat.bytestr(self._value)
92 92
93 93 def tomap(self):
94 94 return self._makemap(self._key)
95 95
96 96 def itermaps(self):
97 97 yield self.tomap()
98 98
99 99 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
100 100 """Wrap data to support both dict-like and string-like operations"""
101 101 prefmt = pycompat.identity
102 102 if fmt is None:
103 103 fmt = '%s=%s'
104 104 prefmt = pycompat.bytestr
105 105 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
106 106 lambda k: fmt % (prefmt(k), prefmt(data[k])))
107 107
108 108 def hybridlist(data, name, fmt=None, gen=None):
109 109 """Wrap data to support both list-like and string-like operations"""
110 110 prefmt = pycompat.identity
111 111 if fmt is None:
112 112 fmt = '%s'
113 113 prefmt = pycompat.bytestr
114 114 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
115 115
116 116 def unwraphybrid(thing):
117 117 """Return an object which can be stringified possibly by using a legacy
118 118 template"""
119 119 gen = getattr(thing, 'gen', None)
120 120 if gen is None:
121 121 return thing
122 122 if callable(gen):
123 123 return gen()
124 124 return gen
125 125
126 126 def unwrapvalue(thing):
127 127 """Move the inner value object out of the wrapper"""
128 128 if not util.safehasattr(thing, '_value'):
129 129 return thing
130 130 return thing._value
131 131
132 132 def wraphybridvalue(container, key, value):
133 133 """Wrap an element of hybrid container to be mappable
134 134
135 135 The key is passed to the makemap function of the given container, which
136 136 should be an item generated by iter(container).
137 137 """
138 138 makemap = getattr(container, '_makemap', None)
139 139 if makemap is None:
140 140 return value
141 141 if util.safehasattr(value, '_makemap'):
142 142 # a nested hybrid list/dict, which has its own way of map operation
143 143 return value
144 144 return mappable(None, key, value, makemap)
145 145
146 146 def compatdict(context, mapping, name, data, key='key', value='value',
147 147 fmt=None, plural=None, separator=' '):
148 148 """Wrap data like hybriddict(), but also supports old-style list template
149 149
150 150 This exists for backward compatibility with the old-style template. Use
151 151 hybriddict() for new template keywords.
152 152 """
153 153 c = [{key: k, value: v} for k, v in data.iteritems()]
154 154 f = _showcompatlist(context, mapping, name, c, plural, separator)
155 155 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
156 156
157 157 def compatlist(context, mapping, name, data, element=None, fmt=None,
158 158 plural=None, separator=' '):
159 159 """Wrap data like hybridlist(), but also supports old-style list template
160 160
161 161 This exists for backward compatibility with the old-style template. Use
162 162 hybridlist() for new template keywords.
163 163 """
164 164 f = _showcompatlist(context, mapping, name, data, plural, separator)
165 165 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
166 166
167 167 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
168 168 """Return a generator that renders old-style list template
169 169
170 170 name is name of key in template map.
171 171 values is list of strings or dicts.
172 172 plural is plural of name, if not simply name + 's'.
173 173 separator is used to join values as a string
174 174
175 175 expansion works like this, given name 'foo'.
176 176
177 177 if values is empty, expand 'no_foos'.
178 178
179 179 if 'foo' not in template map, return values as a string,
180 180 joined by 'separator'.
181 181
182 182 expand 'start_foos'.
183 183
184 184 for each value, expand 'foo'. if 'last_foo' in template
185 185 map, expand it instead of 'foo' for last key.
186 186
187 187 expand 'end_foos'.
188 188 """
189 189 if not plural:
190 190 plural = name + 's'
191 191 if not values:
192 192 noname = 'no_' + plural
193 193 if context.preload(noname):
194 194 yield context.process(noname, mapping)
195 195 return
196 196 if not context.preload(name):
197 197 if isinstance(values[0], bytes):
198 198 yield separator.join(values)
199 199 else:
200 200 for v in values:
201 201 r = dict(v)
202 202 r.update(mapping)
203 203 yield r
204 204 return
205 205 startname = 'start_' + plural
206 206 if context.preload(startname):
207 207 yield context.process(startname, mapping)
208 208 def one(v, tag=name):
209 209 vmapping = {}
210 210 try:
211 211 vmapping.update(v)
212 212 # Python 2 raises ValueError if the type of v is wrong. Python
213 213 # 3 raises TypeError.
214 214 except (AttributeError, TypeError, ValueError):
215 215 try:
216 216 # Python 2 raises ValueError trying to destructure an e.g.
217 217 # bytes. Python 3 raises TypeError.
218 218 for a, b in v:
219 219 vmapping[a] = b
220 220 except (TypeError, ValueError):
221 221 vmapping[name] = v
222 222 vmapping = context.overlaymap(mapping, vmapping)
223 223 return context.process(tag, vmapping)
224 224 lastname = 'last_' + name
225 225 if context.preload(lastname):
226 226 last = values.pop()
227 227 else:
228 228 last = None
229 229 for v in values:
230 230 yield one(v)
231 231 if last is not None:
232 232 yield one(last, tag=lastname)
233 233 endname = 'end_' + plural
234 234 if context.preload(endname):
235 235 yield context.process(endname, mapping)
236 236
237 237 def stringify(thing):
238 238 """Turn values into bytes by converting into text and concatenating them"""
239 239 thing = unwraphybrid(thing)
240 240 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
241 241 if isinstance(thing, str):
242 242 # This is only reachable on Python 3 (otherwise
243 243 # isinstance(thing, bytes) would have been true), and is
244 244 # here to prevent infinite recursion bugs on Python 3.
245 245 raise error.ProgrammingError(
246 246 'stringify got unexpected unicode string: %r' % thing)
247 247 return "".join([stringify(t) for t in thing if t is not None])
248 248 if thing is None:
249 249 return ""
250 250 return pycompat.bytestr(thing)
251 251
252 252 def findsymbolicname(arg):
253 253 """Find symbolic name for the given compiled expression; returns None
254 254 if nothing found reliably"""
255 255 while True:
256 256 func, data = arg
257 257 if func is runsymbol:
258 258 return data
259 259 elif func is runfilter:
260 260 arg = data[0]
261 261 else:
262 262 return None
263 263
264 264 def evalrawexp(context, mapping, arg):
265 265 """Evaluate given argument as a bare template object which may require
266 266 further processing (such as folding generator of strings)"""
267 267 func, data = arg
268 268 return func(context, mapping, data)
269 269
270 270 def evalfuncarg(context, mapping, arg):
271 271 """Evaluate given argument as value type"""
272 272 thing = evalrawexp(context, mapping, arg)
273 273 thing = unwrapvalue(thing)
274 274 # evalrawexp() may return string, generator of strings or arbitrary object
275 275 # such as date tuple, but filter does not want generator.
276 276 if isinstance(thing, types.GeneratorType):
277 277 thing = stringify(thing)
278 278 return thing
279 279
280 280 def evalboolean(context, mapping, arg):
281 281 """Evaluate given argument as boolean, but also takes boolean literals"""
282 282 func, data = arg
283 283 if func is runsymbol:
284 284 thing = func(context, mapping, data, default=None)
285 285 if thing is None:
286 286 # not a template keyword, takes as a boolean literal
287 287 thing = stringutil.parsebool(data)
288 288 else:
289 289 thing = func(context, mapping, data)
290 290 thing = unwrapvalue(thing)
291 291 if isinstance(thing, bool):
292 292 return thing
293 293 # other objects are evaluated as strings, which means 0 is True, but
294 294 # empty dict/list should be False as they are expected to be ''
295 295 return bool(stringify(thing))
296 296
297 297 def evalinteger(context, mapping, arg, err=None):
298 298 v = evalfuncarg(context, mapping, arg)
299 299 try:
300 300 return int(v)
301 301 except (TypeError, ValueError):
302 302 raise error.ParseError(err or _('not an integer'))
303 303
304 304 def evalstring(context, mapping, arg):
305 305 return stringify(evalrawexp(context, mapping, arg))
306 306
307 307 def evalstringliteral(context, mapping, arg):
308 308 """Evaluate given argument as string template, but returns symbol name
309 309 if it is unknown"""
310 310 func, data = arg
311 311 if func is runsymbol:
312 312 thing = func(context, mapping, data, default=data)
313 313 else:
314 314 thing = func(context, mapping, data)
315 315 return stringify(thing)
316 316
317 317 _evalfuncbytype = {
318 318 bool: evalboolean,
319 319 bytes: evalstring,
320 320 int: evalinteger,
321 321 }
322 322
323 323 def evalastype(context, mapping, arg, typ):
324 324 """Evaluate given argument and coerce its type"""
325 325 try:
326 326 f = _evalfuncbytype[typ]
327 327 except KeyError:
328 328 raise error.ProgrammingError('invalid type specified: %r' % typ)
329 329 return f(context, mapping, arg)
330 330
331 331 def runinteger(context, mapping, data):
332 332 return int(data)
333 333
334 334 def runstring(context, mapping, data):
335 335 return data
336 336
337 337 def _recursivesymbolblocker(key):
338 338 def showrecursion(**args):
339 339 raise error.Abort(_("recursive reference '%s' in template") % key)
340 340 return showrecursion
341 341
342 342 def runsymbol(context, mapping, key, default=''):
343 343 v = context.symbol(mapping, key)
344 344 if v is None:
345 345 # put poison to cut recursion. we can't move this to parsing phase
346 346 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
347 347 safemapping = mapping.copy()
348 348 safemapping[key] = _recursivesymbolblocker(key)
349 349 try:
350 350 v = context.process(key, safemapping)
351 351 except TemplateNotFound:
352 352 v = default
353 353 if callable(v) and getattr(v, '_requires', None) is None:
354 354 # old templatekw: expand all keywords and resources
355 355 # (TODO: deprecate this after porting web template keywords to new API)
356 356 props = {k: context._resources.lookup(context, mapping, k)
357 357 for k in context._resources.knownkeys()}
358 358 # pass context to _showcompatlist() through templatekw._showlist()
359 359 props['templ'] = context
360 360 props.update(mapping)
361 361 return v(**pycompat.strkwargs(props))
362 362 if callable(v):
363 363 # new templatekw
364 364 try:
365 365 return v(context, mapping)
366 366 except ResourceUnavailable:
367 367 # unsupported keyword is mapped to empty just like unknown keyword
368 368 return None
369 369 return v
370 370
371 371 def runtemplate(context, mapping, template):
372 372 for arg in template:
373 373 yield evalrawexp(context, mapping, arg)
374 374
375 375 def runfilter(context, mapping, data):
376 376 arg, filt = data
377 377 thing = evalfuncarg(context, mapping, arg)
378 378 try:
379 379 return filt(thing)
380 380 except (ValueError, AttributeError, TypeError):
381 381 sym = findsymbolicname(arg)
382 382 if sym:
383 383 msg = (_("template filter '%s' is not compatible with keyword '%s'")
384 384 % (pycompat.sysbytes(filt.__name__), sym))
385 385 else:
386 386 msg = (_("incompatible use of template filter '%s'")
387 387 % pycompat.sysbytes(filt.__name__))
388 388 raise error.Abort(msg)
389 389
390 390 def runmap(context, mapping, data):
391 391 darg, targ = data
392 392 d = evalrawexp(context, mapping, darg)
393 393 if util.safehasattr(d, 'itermaps'):
394 394 diter = d.itermaps()
395 395 else:
396 396 try:
397 397 diter = iter(d)
398 398 except TypeError:
399 399 sym = findsymbolicname(darg)
400 400 if sym:
401 401 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
402 402 else:
403 403 raise error.ParseError(_("%r is not iterable") % d)
404 404
405 405 for i, v in enumerate(diter):
406 406 if isinstance(v, dict):
407 407 lm = context.overlaymap(mapping, v)
408 408 lm['index'] = i
409 lm['originalnode'] = mapping.get('node')
410 409 yield evalrawexp(context, lm, targ)
411 410 else:
412 411 # v is not an iterable of dicts, this happen when 'key'
413 412 # has been fully expanded already and format is useless.
414 413 # If so, return the expanded value.
415 414 yield v
416 415
417 416 def runmember(context, mapping, data):
418 417 darg, memb = data
419 418 d = evalrawexp(context, mapping, darg)
420 419 if util.safehasattr(d, 'tomap'):
421 420 lm = context.overlaymap(mapping, d.tomap())
422 421 return runsymbol(context, lm, memb)
423 422 if util.safehasattr(d, 'get'):
424 423 return getdictitem(d, memb)
425 424
426 425 sym = findsymbolicname(darg)
427 426 if sym:
428 427 raise error.ParseError(_("keyword '%s' has no member") % sym)
429 428 else:
430 429 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
431 430
432 431 def runnegate(context, mapping, data):
433 432 data = evalinteger(context, mapping, data,
434 433 _('negation needs an integer argument'))
435 434 return -data
436 435
437 436 def runarithmetic(context, mapping, data):
438 437 func, left, right = data
439 438 left = evalinteger(context, mapping, left,
440 439 _('arithmetic only defined on integers'))
441 440 right = evalinteger(context, mapping, right,
442 441 _('arithmetic only defined on integers'))
443 442 try:
444 443 return func(left, right)
445 444 except ZeroDivisionError:
446 445 raise error.Abort(_('division by zero is not defined'))
447 446
448 447 def getdictitem(dictarg, key):
449 448 val = dictarg.get(key)
450 449 if val is None:
451 450 return
452 451 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now