##// END OF EJS Templates
templater: remove unused context argument from most resourcemapper functions...
Yuya Nishihara -
r39618:28f974d8 default
parent child Browse files
Show More
@@ -1,649 +1,649 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 (
120 120 attr,
121 121 )
122 122
123 123 from . import (
124 124 error,
125 125 pycompat,
126 126 templatefilters,
127 127 templatefuncs,
128 128 templatekw,
129 129 templater,
130 130 templateutil,
131 131 util,
132 132 )
133 133 from .utils import dateutil
134 134
135 135 pickle = util.pickle
136 136
137 137 class _nullconverter(object):
138 138 '''convert non-primitive data types to be processed by formatter'''
139 139
140 140 # set to True if context object should be stored as item
141 141 storecontext = False
142 142
143 143 @staticmethod
144 144 def wrapnested(data, tmpl, sep):
145 145 '''wrap nested data by appropriate type'''
146 146 return data
147 147 @staticmethod
148 148 def formatdate(date, fmt):
149 149 '''convert date tuple to appropriate format'''
150 150 # timestamp can be float, but the canonical form should be int
151 151 ts, tz = date
152 152 return (int(ts), tz)
153 153 @staticmethod
154 154 def formatdict(data, key, value, fmt, sep):
155 155 '''convert dict or key-value pairs to appropriate dict format'''
156 156 # use plain dict instead of util.sortdict so that data can be
157 157 # serialized as a builtin dict in pickle output
158 158 return dict(data)
159 159 @staticmethod
160 160 def formatlist(data, name, fmt, sep):
161 161 '''convert iterable to appropriate list format'''
162 162 return list(data)
163 163
164 164 class baseformatter(object):
165 165 def __init__(self, ui, topic, opts, converter):
166 166 self._ui = ui
167 167 self._topic = topic
168 168 self._opts = opts
169 169 self._converter = converter
170 170 self._item = None
171 171 # function to convert node to string suitable for this output
172 172 self.hexfunc = hex
173 173 def __enter__(self):
174 174 return self
175 175 def __exit__(self, exctype, excvalue, traceback):
176 176 if exctype is None:
177 177 self.end()
178 178 def _showitem(self):
179 179 '''show a formatted item once all data is collected'''
180 180 def startitem(self):
181 181 '''begin an item in the format list'''
182 182 if self._item is not None:
183 183 self._showitem()
184 184 self._item = {}
185 185 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
186 186 '''convert date tuple to appropriate format'''
187 187 return self._converter.formatdate(date, fmt)
188 188 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
189 189 '''convert dict or key-value pairs to appropriate dict format'''
190 190 return self._converter.formatdict(data, key, value, fmt, sep)
191 191 def formatlist(self, data, name, fmt=None, sep=' '):
192 192 '''convert iterable to appropriate list format'''
193 193 # name is mandatory argument for now, but it could be optional if
194 194 # we have default template keyword, e.g. {item}
195 195 return self._converter.formatlist(data, name, fmt, sep)
196 196 def contexthint(self, datafields):
197 197 '''set of context object keys to be required given datafields set'''
198 198 return set()
199 199 def context(self, **ctxs):
200 200 '''insert context objects to be used to render template keywords'''
201 201 ctxs = pycompat.byteskwargs(ctxs)
202 202 assert all(k in {'ctx', 'fctx'} for k in ctxs)
203 203 if self._converter.storecontext:
204 204 self._item.update(ctxs)
205 205 def datahint(self):
206 206 '''set of field names to be referenced'''
207 207 return set()
208 208 def data(self, **data):
209 209 '''insert data into item that's not shown in default output'''
210 210 data = pycompat.byteskwargs(data)
211 211 self._item.update(data)
212 212 def write(self, fields, deftext, *fielddata, **opts):
213 213 '''do default text output while assigning data to item'''
214 214 fieldkeys = fields.split()
215 215 assert len(fieldkeys) == len(fielddata)
216 216 self._item.update(zip(fieldkeys, fielddata))
217 217 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
218 218 '''do conditional write (primarily for plain formatter)'''
219 219 fieldkeys = fields.split()
220 220 assert len(fieldkeys) == len(fielddata)
221 221 self._item.update(zip(fieldkeys, fielddata))
222 222 def plain(self, text, **opts):
223 223 '''show raw text for non-templated mode'''
224 224 def isplain(self):
225 225 '''check for plain formatter usage'''
226 226 return False
227 227 def nested(self, field, tmpl=None, sep=''):
228 228 '''sub formatter to store nested data in the specified field'''
229 229 data = []
230 230 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
231 231 return _nestedformatter(self._ui, self._converter, data)
232 232 def end(self):
233 233 '''end output for the formatter'''
234 234 if self._item is not None:
235 235 self._showitem()
236 236
237 237 def nullformatter(ui, topic, opts):
238 238 '''formatter that prints nothing'''
239 239 return baseformatter(ui, topic, opts, converter=_nullconverter)
240 240
241 241 class _nestedformatter(baseformatter):
242 242 '''build sub items and store them in the parent formatter'''
243 243 def __init__(self, ui, converter, data):
244 244 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
245 245 self._data = data
246 246 def _showitem(self):
247 247 self._data.append(self._item)
248 248
249 249 def _iteritems(data):
250 250 '''iterate key-value pairs in stable order'''
251 251 if isinstance(data, dict):
252 252 return sorted(data.iteritems())
253 253 return data
254 254
255 255 class _plainconverter(object):
256 256 '''convert non-primitive data types to text'''
257 257
258 258 storecontext = False
259 259
260 260 @staticmethod
261 261 def wrapnested(data, tmpl, sep):
262 262 raise error.ProgrammingError('plainformatter should never be nested')
263 263 @staticmethod
264 264 def formatdate(date, fmt):
265 265 '''stringify date tuple in the given format'''
266 266 return dateutil.datestr(date, fmt)
267 267 @staticmethod
268 268 def formatdict(data, key, value, fmt, sep):
269 269 '''stringify key-value pairs separated by sep'''
270 270 prefmt = pycompat.identity
271 271 if fmt is None:
272 272 fmt = '%s=%s'
273 273 prefmt = pycompat.bytestr
274 274 return sep.join(fmt % (prefmt(k), prefmt(v))
275 275 for k, v in _iteritems(data))
276 276 @staticmethod
277 277 def formatlist(data, name, fmt, sep):
278 278 '''stringify iterable separated by sep'''
279 279 prefmt = pycompat.identity
280 280 if fmt is None:
281 281 fmt = '%s'
282 282 prefmt = pycompat.bytestr
283 283 return sep.join(fmt % prefmt(e) for e in data)
284 284
285 285 class plainformatter(baseformatter):
286 286 '''the default text output scheme'''
287 287 def __init__(self, ui, out, topic, opts):
288 288 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
289 289 if ui.debugflag:
290 290 self.hexfunc = hex
291 291 else:
292 292 self.hexfunc = short
293 293 if ui is out:
294 294 self._write = ui.write
295 295 else:
296 296 self._write = lambda s, **opts: out.write(s)
297 297 def startitem(self):
298 298 pass
299 299 def data(self, **data):
300 300 pass
301 301 def write(self, fields, deftext, *fielddata, **opts):
302 302 self._write(deftext % fielddata, **opts)
303 303 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
304 304 '''do conditional write'''
305 305 if cond:
306 306 self._write(deftext % fielddata, **opts)
307 307 def plain(self, text, **opts):
308 308 self._write(text, **opts)
309 309 def isplain(self):
310 310 return True
311 311 def nested(self, field, tmpl=None, sep=''):
312 312 # nested data will be directly written to ui
313 313 return self
314 314 def end(self):
315 315 pass
316 316
317 317 class debugformatter(baseformatter):
318 318 def __init__(self, ui, out, topic, opts):
319 319 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
320 320 self._out = out
321 321 self._out.write("%s = [\n" % self._topic)
322 322 def _showitem(self):
323 323 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
324 324 def end(self):
325 325 baseformatter.end(self)
326 326 self._out.write("]\n")
327 327
328 328 class pickleformatter(baseformatter):
329 329 def __init__(self, ui, out, topic, opts):
330 330 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
331 331 self._out = out
332 332 self._data = []
333 333 def _showitem(self):
334 334 self._data.append(self._item)
335 335 def end(self):
336 336 baseformatter.end(self)
337 337 self._out.write(pickle.dumps(self._data))
338 338
339 339 class jsonformatter(baseformatter):
340 340 def __init__(self, ui, out, topic, opts):
341 341 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
342 342 self._out = out
343 343 self._out.write("[")
344 344 self._first = True
345 345 def _showitem(self):
346 346 if self._first:
347 347 self._first = False
348 348 else:
349 349 self._out.write(",")
350 350
351 351 self._out.write("\n {\n")
352 352 first = True
353 353 for k, v in sorted(self._item.items()):
354 354 if first:
355 355 first = False
356 356 else:
357 357 self._out.write(",\n")
358 358 u = templatefilters.json(v, paranoid=False)
359 359 self._out.write(' "%s": %s' % (k, u))
360 360 self._out.write("\n }")
361 361 def end(self):
362 362 baseformatter.end(self)
363 363 self._out.write("\n]\n")
364 364
365 365 class _templateconverter(object):
366 366 '''convert non-primitive data types to be processed by templater'''
367 367
368 368 storecontext = True
369 369
370 370 @staticmethod
371 371 def wrapnested(data, tmpl, sep):
372 372 '''wrap nested data by templatable type'''
373 373 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
374 374 @staticmethod
375 375 def formatdate(date, fmt):
376 376 '''return date tuple'''
377 377 return templateutil.date(date)
378 378 @staticmethod
379 379 def formatdict(data, key, value, fmt, sep):
380 380 '''build object that can be evaluated as either plain string or dict'''
381 381 data = util.sortdict(_iteritems(data))
382 382 def f():
383 383 yield _plainconverter.formatdict(data, key, value, fmt, sep)
384 384 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
385 385 gen=f)
386 386 @staticmethod
387 387 def formatlist(data, name, fmt, sep):
388 388 '''build object that can be evaluated as either plain string or list'''
389 389 data = list(data)
390 390 def f():
391 391 yield _plainconverter.formatlist(data, name, fmt, sep)
392 392 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
393 393
394 394 class templateformatter(baseformatter):
395 395 def __init__(self, ui, out, topic, opts):
396 396 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
397 397 self._out = out
398 398 spec = lookuptemplate(ui, topic, opts.get('template', ''))
399 399 self._tref = spec.ref
400 400 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
401 401 resources=templateresources(ui),
402 402 cache=templatekw.defaulttempl)
403 403 self._parts = templatepartsmap(spec, self._t,
404 404 ['docheader', 'docfooter', 'separator'])
405 405 self._counter = itertools.count()
406 406 self._renderitem('docheader', {})
407 407
408 408 def _showitem(self):
409 409 item = self._item.copy()
410 410 item['index'] = index = next(self._counter)
411 411 if index > 0:
412 412 self._renderitem('separator', {})
413 413 self._renderitem(self._tref, item)
414 414
415 415 def _renderitem(self, part, item):
416 416 if part not in self._parts:
417 417 return
418 418 ref = self._parts[part]
419 419 self._out.write(self._t.render(ref, item))
420 420
421 421 @util.propertycache
422 422 def _symbolsused(self):
423 423 return self._t.symbolsused(self._tref)
424 424
425 425 def contexthint(self, datafields):
426 426 '''set of context object keys to be required by the template, given
427 427 datafields overridden by immediate values'''
428 428 requires = set()
429 429 ksyms, fsyms = self._symbolsused
430 430 ksyms = ksyms - set(datafields.split()) # exclude immediate fields
431 431 symtables = [(ksyms, templatekw.keywords),
432 432 (fsyms, templatefuncs.funcs)]
433 433 for syms, table in symtables:
434 434 for k in syms:
435 435 f = table.get(k)
436 436 if not f:
437 437 continue
438 438 requires.update(getattr(f, '_requires', ()))
439 439 if 'repo' in requires:
440 440 requires.add('ctx') # there's no API to pass repo to formatter
441 441 return requires & {'ctx', 'fctx'}
442 442
443 443 def datahint(self):
444 444 '''set of field names to be referenced from the template'''
445 445 return self._symbolsused[0]
446 446
447 447 def end(self):
448 448 baseformatter.end(self)
449 449 self._renderitem('docfooter', {})
450 450
451 451 @attr.s(frozen=True)
452 452 class templatespec(object):
453 453 ref = attr.ib()
454 454 tmpl = attr.ib()
455 455 mapfile = attr.ib()
456 456
457 457 def lookuptemplate(ui, topic, tmpl):
458 458 """Find the template matching the given -T/--template spec 'tmpl'
459 459
460 460 'tmpl' can be any of the following:
461 461
462 462 - a literal template (e.g. '{rev}')
463 463 - a map-file name or path (e.g. 'changelog')
464 464 - a reference to [templates] in config file
465 465 - a path to raw template file
466 466
467 467 A map file defines a stand-alone template environment. If a map file
468 468 selected, all templates defined in the file will be loaded, and the
469 469 template matching the given topic will be rendered. Aliases won't be
470 470 loaded from user config, but from the map file.
471 471
472 472 If no map file selected, all templates in [templates] section will be
473 473 available as well as aliases in [templatealias].
474 474 """
475 475
476 476 # looks like a literal template?
477 477 if '{' in tmpl:
478 478 return templatespec('', tmpl, None)
479 479
480 480 # perhaps a stock style?
481 481 if not os.path.split(tmpl)[0]:
482 482 mapname = (templater.templatepath('map-cmdline.' + tmpl)
483 483 or templater.templatepath(tmpl))
484 484 if mapname and os.path.isfile(mapname):
485 485 return templatespec(topic, None, mapname)
486 486
487 487 # perhaps it's a reference to [templates]
488 488 if ui.config('templates', tmpl):
489 489 return templatespec(tmpl, None, None)
490 490
491 491 if tmpl == 'list':
492 492 ui.write(_("available styles: %s\n") % templater.stylelist())
493 493 raise error.Abort(_("specify a template"))
494 494
495 495 # perhaps it's a path to a map or a template
496 496 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
497 497 # is it a mapfile for a style?
498 498 if os.path.basename(tmpl).startswith("map-"):
499 499 return templatespec(topic, None, os.path.realpath(tmpl))
500 500 with util.posixfile(tmpl, 'rb') as f:
501 501 tmpl = f.read()
502 502 return templatespec('', tmpl, None)
503 503
504 504 # constant string?
505 505 return templatespec('', tmpl, None)
506 506
507 507 def templatepartsmap(spec, t, partnames):
508 508 """Create a mapping of {part: ref}"""
509 509 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
510 510 if spec.mapfile:
511 511 partsmap.update((p, p) for p in partnames if p in t)
512 512 elif spec.ref:
513 513 for part in partnames:
514 514 ref = '%s:%s' % (spec.ref, part) # select config sub-section
515 515 if ref in t:
516 516 partsmap[part] = ref
517 517 return partsmap
518 518
519 519 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
520 520 """Create a templater from either a literal template or loading from
521 521 a map file"""
522 522 assert not (spec.tmpl and spec.mapfile)
523 523 if spec.mapfile:
524 524 frommapfile = templater.templater.frommapfile
525 525 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
526 526 cache=cache)
527 527 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
528 528 cache=cache)
529 529
530 530 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
531 531 """Create a templater from a string template 'tmpl'"""
532 532 aliases = ui.configitems('templatealias')
533 533 t = templater.templater(defaults=defaults, resources=resources,
534 534 cache=cache, aliases=aliases)
535 535 t.cache.update((k, templater.unquotestring(v))
536 536 for k, v in ui.configitems('templates'))
537 537 if tmpl:
538 538 t.cache[''] = tmpl
539 539 return t
540 540
541 541 class templateresources(templater.resourcemapper):
542 542 """Resource mapper designed for the default templatekw and function"""
543 543
544 544 def __init__(self, ui, repo=None):
545 545 self._resmap = {
546 546 'cache': {}, # for templatekw/funcs to store reusable data
547 547 'repo': repo,
548 548 'ui': ui,
549 549 }
550 550
551 def availablekeys(self, context, mapping):
551 def availablekeys(self, mapping):
552 552 return {k for k, g in self._gettermap.iteritems()
553 if g(self, context, mapping, k) is not None}
553 if g(self, mapping, k) is not None}
554 554
555 555 def knownkeys(self):
556 556 return self._knownkeys
557 557
558 def lookup(self, context, mapping, key):
558 def lookup(self, mapping, key):
559 559 get = self._gettermap.get(key)
560 560 if not get:
561 561 return None
562 return get(self, context, mapping, key)
562 return get(self, mapping, key)
563 563
564 564 def populatemap(self, context, origmapping, newmapping):
565 565 mapping = {}
566 566 if self._hasctx(newmapping):
567 567 mapping['revcache'] = {} # per-ctx cache
568 568 if (('node' in origmapping or self._hasctx(origmapping))
569 569 and ('node' in newmapping or self._hasctx(newmapping))):
570 570 orignode = templateutil.runsymbol(context, origmapping, 'node')
571 571 mapping['originalnode'] = orignode
572 572 return mapping
573 573
574 def _getsome(self, context, mapping, key):
574 def _getsome(self, mapping, key):
575 575 v = mapping.get(key)
576 576 if v is not None:
577 577 return v
578 578 return self._resmap.get(key)
579 579
580 580 def _hasctx(self, mapping):
581 581 return 'ctx' in mapping or 'fctx' in mapping
582 582
583 def _getctx(self, context, mapping, key):
583 def _getctx(self, mapping, key):
584 584 ctx = mapping.get('ctx')
585 585 if ctx is not None:
586 586 return ctx
587 587 fctx = mapping.get('fctx')
588 588 if fctx is not None:
589 589 return fctx.changectx()
590 590
591 def _getrepo(self, context, mapping, key):
592 ctx = self._getctx(context, mapping, 'ctx')
591 def _getrepo(self, mapping, key):
592 ctx = self._getctx(mapping, 'ctx')
593 593 if ctx is not None:
594 594 return ctx.repo()
595 return self._getsome(context, mapping, key)
595 return self._getsome(mapping, key)
596 596
597 597 _gettermap = {
598 598 'cache': _getsome,
599 599 'ctx': _getctx,
600 600 'fctx': _getsome,
601 601 'repo': _getrepo,
602 602 'revcache': _getsome,
603 603 'ui': _getsome,
604 604 }
605 605 _knownkeys = set(_gettermap.keys())
606 606
607 607 def formatter(ui, out, topic, opts):
608 608 template = opts.get("template", "")
609 609 if template == "json":
610 610 return jsonformatter(ui, out, topic, opts)
611 611 elif template == "pickle":
612 612 return pickleformatter(ui, out, topic, opts)
613 613 elif template == "debug":
614 614 return debugformatter(ui, out, topic, opts)
615 615 elif template != "":
616 616 return templateformatter(ui, out, topic, opts)
617 617 # developer config: ui.formatdebug
618 618 elif ui.configbool('ui', 'formatdebug'):
619 619 return debugformatter(ui, out, topic, opts)
620 620 # deprecated config: ui.formatjson
621 621 elif ui.configbool('ui', 'formatjson'):
622 622 return jsonformatter(ui, out, topic, opts)
623 623 return plainformatter(ui, out, topic, opts)
624 624
625 625 @contextlib.contextmanager
626 626 def openformatter(ui, filename, topic, opts):
627 627 """Create a formatter that writes outputs to the specified file
628 628
629 629 Must be invoked using the 'with' statement.
630 630 """
631 631 with util.posixfile(filename, 'wb') as out:
632 632 with formatter(ui, out, topic, opts) as fm:
633 633 yield fm
634 634
635 635 @contextlib.contextmanager
636 636 def _neverending(fm):
637 637 yield fm
638 638
639 639 def maybereopen(fm, filename):
640 640 """Create a formatter backed by file if filename specified, else return
641 641 the given formatter
642 642
643 643 Must be invoked using the 'with' statement. This will never call fm.end()
644 644 of the given formatter.
645 645 """
646 646 if filename:
647 647 return openformatter(fm._ui, filename, fm._topic, fm._opts)
648 648 else:
649 649 return _neverending(fm)
@@ -1,985 +1,985 b''
1 1 # templater.py - template expansion for output
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 """Slightly complicated template engine for commands and hgweb
9 9
10 10 This module provides low-level interface to the template engine. See the
11 11 formatter and cmdutil modules if you are looking for high-level functions
12 12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13 13
14 14 Internal Data Types
15 15 -------------------
16 16
17 17 Template keywords and functions take a dictionary of current symbols and
18 18 resources (a "mapping") and return result. Inputs and outputs must be one
19 19 of the following data types:
20 20
21 21 bytes
22 22 a byte string, which is generally a human-readable text in local encoding.
23 23
24 24 generator
25 25 a lazily-evaluated byte string, which is a possibly nested generator of
26 26 values of any printable types, and will be folded by ``stringify()``
27 27 or ``flatten()``.
28 28
29 29 None
30 30 sometimes represents an empty value, which can be stringified to ''.
31 31
32 32 True, False, int, float
33 33 can be stringified as such.
34 34
35 35 wrappedbytes, wrappedvalue
36 36 a wrapper for the above printable types.
37 37
38 38 date
39 39 represents a (unixtime, offset) tuple.
40 40
41 41 hybrid
42 42 represents a list/dict of printable values, which can also be converted
43 43 to mappings by % operator.
44 44
45 45 hybriditem
46 46 represents a scalar printable value, also supports % operator.
47 47
48 48 mappinggenerator, mappinglist
49 49 represents mappings (i.e. a list of dicts), which may have default
50 50 output format.
51 51
52 52 mappedgenerator
53 53 a lazily-evaluated list of byte strings, which is e.g. a result of %
54 54 operation.
55 55 """
56 56
57 57 from __future__ import absolute_import, print_function
58 58
59 59 import abc
60 60 import os
61 61
62 62 from .i18n import _
63 63 from . import (
64 64 config,
65 65 encoding,
66 66 error,
67 67 parser,
68 68 pycompat,
69 69 templatefilters,
70 70 templatefuncs,
71 71 templateutil,
72 72 util,
73 73 )
74 74 from .utils import (
75 75 stringutil,
76 76 )
77 77
78 78 # template parsing
79 79
80 80 elements = {
81 81 # token-type: binding-strength, primary, prefix, infix, suffix
82 82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
83 83 ".": (18, None, None, (".", 18), None),
84 84 "%": (15, None, None, ("%", 15), None),
85 85 "|": (15, None, None, ("|", 15), None),
86 86 "*": (5, None, None, ("*", 5), None),
87 87 "/": (5, None, None, ("/", 5), None),
88 88 "+": (4, None, None, ("+", 4), None),
89 89 "-": (4, None, ("negate", 19), ("-", 4), None),
90 90 "=": (3, None, None, ("keyvalue", 3), None),
91 91 ",": (2, None, None, ("list", 2), None),
92 92 ")": (0, None, None, None, None),
93 93 "integer": (0, "integer", None, None, None),
94 94 "symbol": (0, "symbol", None, None, None),
95 95 "string": (0, "string", None, None, None),
96 96 "template": (0, "template", None, None, None),
97 97 "end": (0, None, None, None, None),
98 98 }
99 99
100 100 def tokenize(program, start, end, term=None):
101 101 """Parse a template expression into a stream of tokens, which must end
102 102 with term if specified"""
103 103 pos = start
104 104 program = pycompat.bytestr(program)
105 105 while pos < end:
106 106 c = program[pos]
107 107 if c.isspace(): # skip inter-token whitespace
108 108 pass
109 109 elif c in "(=,).%|+-*/": # handle simple operators
110 110 yield (c, None, pos)
111 111 elif c in '"\'': # handle quoted templates
112 112 s = pos + 1
113 113 data, pos = _parsetemplate(program, s, end, c)
114 114 yield ('template', data, s)
115 115 pos -= 1
116 116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
117 117 # handle quoted strings
118 118 c = program[pos + 1]
119 119 s = pos = pos + 2
120 120 while pos < end: # find closing quote
121 121 d = program[pos]
122 122 if d == '\\': # skip over escaped characters
123 123 pos += 2
124 124 continue
125 125 if d == c:
126 126 yield ('string', program[s:pos], s)
127 127 break
128 128 pos += 1
129 129 else:
130 130 raise error.ParseError(_("unterminated string"), s)
131 131 elif c.isdigit():
132 132 s = pos
133 133 while pos < end:
134 134 d = program[pos]
135 135 if not d.isdigit():
136 136 break
137 137 pos += 1
138 138 yield ('integer', program[s:pos], s)
139 139 pos -= 1
140 140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
141 141 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
142 142 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
143 143 # where some of nested templates were preprocessed as strings and
144 144 # then compiled. therefore, \"...\" was allowed. (issue4733)
145 145 #
146 146 # processing flow of _evalifliteral() at 5ab28a2e9962:
147 147 # outer template string -> stringify() -> compiletemplate()
148 148 # ------------------------ ------------ ------------------
149 149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
150 150 # ~~~~~~~~
151 151 # escaped quoted string
152 152 if c == 'r':
153 153 pos += 1
154 154 token = 'string'
155 155 else:
156 156 token = 'template'
157 157 quote = program[pos:pos + 2]
158 158 s = pos = pos + 2
159 159 while pos < end: # find closing escaped quote
160 160 if program.startswith('\\\\\\', pos, end):
161 161 pos += 4 # skip over double escaped characters
162 162 continue
163 163 if program.startswith(quote, pos, end):
164 164 # interpret as if it were a part of an outer string
165 165 data = parser.unescapestr(program[s:pos])
166 166 if token == 'template':
167 167 data = _parsetemplate(data, 0, len(data))[0]
168 168 yield (token, data, s)
169 169 pos += 1
170 170 break
171 171 pos += 1
172 172 else:
173 173 raise error.ParseError(_("unterminated string"), s)
174 174 elif c.isalnum() or c in '_':
175 175 s = pos
176 176 pos += 1
177 177 while pos < end: # find end of symbol
178 178 d = program[pos]
179 179 if not (d.isalnum() or d == "_"):
180 180 break
181 181 pos += 1
182 182 sym = program[s:pos]
183 183 yield ('symbol', sym, s)
184 184 pos -= 1
185 185 elif c == term:
186 186 yield ('end', None, pos)
187 187 return
188 188 else:
189 189 raise error.ParseError(_("syntax error"), pos)
190 190 pos += 1
191 191 if term:
192 192 raise error.ParseError(_("unterminated template expansion"), start)
193 193 yield ('end', None, pos)
194 194
195 195 def _parsetemplate(tmpl, start, stop, quote=''):
196 196 r"""
197 197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
198 198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
199 199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
200 200 ([('string', 'foo'), ('symbol', 'bar')], 9)
201 201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
202 202 ([('string', 'foo')], 4)
203 203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
204 204 ([('string', 'foo"'), ('string', 'bar')], 9)
205 205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
206 206 ([('string', 'foo\\')], 6)
207 207 """
208 208 parsed = []
209 209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
210 210 if typ == 'string':
211 211 parsed.append((typ, val))
212 212 elif typ == 'template':
213 213 parsed.append(val)
214 214 elif typ == 'end':
215 215 return parsed, pos
216 216 else:
217 217 raise error.ProgrammingError('unexpected type: %s' % typ)
218 218 raise error.ProgrammingError('unterminated scanning of template')
219 219
220 220 def scantemplate(tmpl, raw=False):
221 221 r"""Scan (type, start, end) positions of outermost elements in template
222 222
223 223 If raw=True, a backslash is not taken as an escape character just like
224 224 r'' string in Python. Note that this is different from r'' literal in
225 225 template in that no template fragment can appear in r'', e.g. r'{foo}'
226 226 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
227 227 'foo'.
228 228
229 229 >>> list(scantemplate(b'foo{bar}"baz'))
230 230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
231 231 >>> list(scantemplate(b'outer{"inner"}outer'))
232 232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
233 233 >>> list(scantemplate(b'foo\\{escaped}'))
234 234 [('string', 0, 5), ('string', 5, 13)]
235 235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
236 236 [('string', 0, 4), ('template', 4, 13)]
237 237 """
238 238 last = None
239 239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
240 240 if last:
241 241 yield last + (pos,)
242 242 if typ == 'end':
243 243 return
244 244 else:
245 245 last = (typ, pos)
246 246 raise error.ProgrammingError('unterminated scanning of template')
247 247
248 248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
249 249 """Parse template string into chunks of strings and template expressions"""
250 250 sepchars = '{' + quote
251 251 unescape = [parser.unescapestr, pycompat.identity][raw]
252 252 pos = start
253 253 p = parser.parser(elements)
254 254 try:
255 255 while pos < stop:
256 256 n = min((tmpl.find(c, pos, stop)
257 257 for c in pycompat.bytestr(sepchars)),
258 258 key=lambda n: (n < 0, n))
259 259 if n < 0:
260 260 yield ('string', unescape(tmpl[pos:stop]), pos)
261 261 pos = stop
262 262 break
263 263 c = tmpl[n:n + 1]
264 264 bs = 0 # count leading backslashes
265 265 if not raw:
266 266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
267 267 if bs % 2 == 1:
268 268 # escaped (e.g. '\{', '\\\{', but not '\\{')
269 269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
270 270 pos = n + 1
271 271 continue
272 272 if n > pos:
273 273 yield ('string', unescape(tmpl[pos:n]), pos)
274 274 if c == quote:
275 275 yield ('end', None, n + 1)
276 276 return
277 277
278 278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
279 279 if not tmpl.startswith('}', pos):
280 280 raise error.ParseError(_("invalid token"), pos)
281 281 yield ('template', parseres, n)
282 282 pos += 1
283 283
284 284 if quote:
285 285 raise error.ParseError(_("unterminated string"), start)
286 286 except error.ParseError as inst:
287 287 if len(inst.args) > 1: # has location
288 288 loc = inst.args[1]
289 289 # Offset the caret location by the number of newlines before the
290 290 # location of the error, since we will replace one-char newlines
291 291 # with the two-char literal r'\n'.
292 292 offset = tmpl[:loc].count('\n')
293 293 tmpl = tmpl.replace('\n', br'\n')
294 294 # We want the caret to point to the place in the template that
295 295 # failed to parse, but in a hint we get a open paren at the
296 296 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
297 297 # to line up the caret with the location of the error.
298 298 inst.hint = (tmpl + '\n'
299 299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
300 300 raise
301 301 yield ('end', None, pos)
302 302
303 303 def _unnesttemplatelist(tree):
304 304 """Expand list of templates to node tuple
305 305
306 306 >>> def f(tree):
307 307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
308 308 >>> f((b'template', []))
309 309 (string '')
310 310 >>> f((b'template', [(b'string', b'foo')]))
311 311 (string 'foo')
312 312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
313 313 (template
314 314 (string 'foo')
315 315 (symbol 'rev'))
316 316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
317 317 (template
318 318 (symbol 'rev'))
319 319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
320 320 (string 'foo')
321 321 """
322 322 if not isinstance(tree, tuple):
323 323 return tree
324 324 op = tree[0]
325 325 if op != 'template':
326 326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
327 327
328 328 assert len(tree) == 2
329 329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
330 330 if not xs:
331 331 return ('string', '') # empty template ""
332 332 elif len(xs) == 1 and xs[0][0] == 'string':
333 333 return xs[0] # fast path for string with no template fragment "x"
334 334 else:
335 335 return (op,) + xs
336 336
337 337 def parse(tmpl):
338 338 """Parse template string into tree"""
339 339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
340 340 assert pos == len(tmpl), 'unquoted template should be consumed'
341 341 return _unnesttemplatelist(('template', parsed))
342 342
343 343 def _parseexpr(expr):
344 344 """Parse a template expression into tree
345 345
346 346 >>> _parseexpr(b'"foo"')
347 347 ('string', 'foo')
348 348 >>> _parseexpr(b'foo(bar)')
349 349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
350 350 >>> _parseexpr(b'foo(')
351 351 Traceback (most recent call last):
352 352 ...
353 353 ParseError: ('not a prefix: end', 4)
354 354 >>> _parseexpr(b'"foo" "bar"')
355 355 Traceback (most recent call last):
356 356 ...
357 357 ParseError: ('invalid token', 7)
358 358 """
359 359 p = parser.parser(elements)
360 360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
361 361 if pos != len(expr):
362 362 raise error.ParseError(_('invalid token'), pos)
363 363 return _unnesttemplatelist(tree)
364 364
365 365 def prettyformat(tree):
366 366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
367 367
368 368 def compileexp(exp, context, curmethods):
369 369 """Compile parsed template tree to (func, data) pair"""
370 370 if not exp:
371 371 raise error.ParseError(_("missing argument"))
372 372 t = exp[0]
373 373 if t in curmethods:
374 374 return curmethods[t](exp, context)
375 375 raise error.ParseError(_("unknown method '%s'") % t)
376 376
377 377 # template evaluation
378 378
379 379 def getsymbol(exp):
380 380 if exp[0] == 'symbol':
381 381 return exp[1]
382 382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
383 383
384 384 def getlist(x):
385 385 if not x:
386 386 return []
387 387 if x[0] == 'list':
388 388 return getlist(x[1]) + [x[2]]
389 389 return [x]
390 390
391 391 def gettemplate(exp, context):
392 392 """Compile given template tree or load named template from map file;
393 393 returns (func, data) pair"""
394 394 if exp[0] in ('template', 'string'):
395 395 return compileexp(exp, context, methods)
396 396 if exp[0] == 'symbol':
397 397 # unlike runsymbol(), here 'symbol' is always taken as template name
398 398 # even if it exists in mapping. this allows us to override mapping
399 399 # by web templates, e.g. 'changelogtag' is redefined in map file.
400 400 return context._load(exp[1])
401 401 raise error.ParseError(_("expected template specifier"))
402 402
403 403 def _runrecursivesymbol(context, mapping, key):
404 404 raise error.Abort(_("recursive reference '%s' in template") % key)
405 405
406 406 def buildtemplate(exp, context):
407 407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
408 408 return (templateutil.runtemplate, ctmpl)
409 409
410 410 def buildfilter(exp, context):
411 411 n = getsymbol(exp[2])
412 412 if n in context._filters:
413 413 filt = context._filters[n]
414 414 arg = compileexp(exp[1], context, methods)
415 415 return (templateutil.runfilter, (arg, filt))
416 416 if n in context._funcs:
417 417 f = context._funcs[n]
418 418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
419 419 return (f, args)
420 420 raise error.ParseError(_("unknown function '%s'") % n)
421 421
422 422 def buildmap(exp, context):
423 423 darg = compileexp(exp[1], context, methods)
424 424 targ = gettemplate(exp[2], context)
425 425 return (templateutil.runmap, (darg, targ))
426 426
427 427 def buildmember(exp, context):
428 428 darg = compileexp(exp[1], context, methods)
429 429 memb = getsymbol(exp[2])
430 430 return (templateutil.runmember, (darg, memb))
431 431
432 432 def buildnegate(exp, context):
433 433 arg = compileexp(exp[1], context, exprmethods)
434 434 return (templateutil.runnegate, arg)
435 435
436 436 def buildarithmetic(exp, context, func):
437 437 left = compileexp(exp[1], context, exprmethods)
438 438 right = compileexp(exp[2], context, exprmethods)
439 439 return (templateutil.runarithmetic, (func, left, right))
440 440
441 441 def buildfunc(exp, context):
442 442 n = getsymbol(exp[1])
443 443 if n in context._funcs:
444 444 f = context._funcs[n]
445 445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
446 446 return (f, args)
447 447 if n in context._filters:
448 448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
449 449 if len(args) != 1:
450 450 raise error.ParseError(_("filter %s expects one argument") % n)
451 451 f = context._filters[n]
452 452 return (templateutil.runfilter, (args[0], f))
453 453 raise error.ParseError(_("unknown function '%s'") % n)
454 454
455 455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
456 456 """Compile parsed tree of function arguments into list or dict of
457 457 (func, data) pairs
458 458
459 459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
460 460 >>> def fargs(expr, argspec):
461 461 ... x = _parseexpr(expr)
462 462 ... n = getsymbol(x[1])
463 463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
464 464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
465 465 ['l', 'k']
466 466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
467 467 >>> list(args.keys()), list(args[b'opts'].keys())
468 468 (['opts'], ['opts', 'k'])
469 469 """
470 470 def compiledict(xs):
471 471 return util.sortdict((k, compileexp(x, context, curmethods))
472 472 for k, x in xs.iteritems())
473 473 def compilelist(xs):
474 474 return [compileexp(x, context, curmethods) for x in xs]
475 475
476 476 if not argspec:
477 477 # filter or function with no argspec: return list of positional args
478 478 return compilelist(getlist(exp))
479 479
480 480 # function with argspec: return dict of named args
481 481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
482 482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
483 483 keyvaluenode='keyvalue', keynode='symbol')
484 484 compargs = util.sortdict()
485 485 if varkey:
486 486 compargs[varkey] = compilelist(treeargs.pop(varkey))
487 487 if optkey:
488 488 compargs[optkey] = compiledict(treeargs.pop(optkey))
489 489 compargs.update(compiledict(treeargs))
490 490 return compargs
491 491
492 492 def buildkeyvaluepair(exp, content):
493 493 raise error.ParseError(_("can't use a key-value pair in this context"))
494 494
495 495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
496 496 exprmethods = {
497 497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
498 498 "string": lambda e, c: (templateutil.runstring, e[1]),
499 499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
500 500 "template": buildtemplate,
501 501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
502 502 ".": buildmember,
503 503 "|": buildfilter,
504 504 "%": buildmap,
505 505 "func": buildfunc,
506 506 "keyvalue": buildkeyvaluepair,
507 507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
508 508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
509 509 "negate": buildnegate,
510 510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
511 511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
512 512 }
513 513
514 514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
515 515 methods = exprmethods.copy()
516 516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
517 517
518 518 class _aliasrules(parser.basealiasrules):
519 519 """Parsing and expansion rule set of template aliases"""
520 520 _section = _('template alias')
521 521 _parse = staticmethod(_parseexpr)
522 522
523 523 @staticmethod
524 524 def _trygetfunc(tree):
525 525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
526 526 None"""
527 527 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 528 return tree[1][1], getlist(tree[2])
529 529 if tree[0] == '|' and tree[2][0] == 'symbol':
530 530 return tree[2][1], [tree[1]]
531 531
532 532 def expandaliases(tree, aliases):
533 533 """Return new tree of aliases are expanded"""
534 534 aliasmap = _aliasrules.buildmap(aliases)
535 535 return _aliasrules.expand(aliasmap, tree)
536 536
537 537 # template engine
538 538
539 539 def unquotestring(s):
540 540 '''unwrap quotes if any; otherwise returns unmodified string'''
541 541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
542 542 return s
543 543 return s[1:-1]
544 544
545 545 class resourcemapper(object):
546 546 """Mapper of internal template resources"""
547 547
548 548 __metaclass__ = abc.ABCMeta
549 549
550 550 @abc.abstractmethod
551 def availablekeys(self, context, mapping):
551 def availablekeys(self, mapping):
552 552 """Return a set of available resource keys based on the given mapping"""
553 553
554 554 @abc.abstractmethod
555 555 def knownkeys(self):
556 556 """Return a set of supported resource keys"""
557 557
558 558 @abc.abstractmethod
559 def lookup(self, context, mapping, key):
559 def lookup(self, mapping, key):
560 560 """Return a resource for the key if available; otherwise None"""
561 561
562 562 @abc.abstractmethod
563 563 def populatemap(self, context, origmapping, newmapping):
564 564 """Return a dict of additional mapping items which should be paired
565 565 with the given new mapping"""
566 566
567 567 class nullresourcemapper(resourcemapper):
568 def availablekeys(self, context, mapping):
568 def availablekeys(self, mapping):
569 569 return set()
570 570
571 571 def knownkeys(self):
572 572 return set()
573 573
574 def lookup(self, context, mapping, key):
574 def lookup(self, mapping, key):
575 575 return None
576 576
577 577 def populatemap(self, context, origmapping, newmapping):
578 578 return {}
579 579
580 580 class engine(object):
581 581 '''template expansion engine.
582 582
583 583 template expansion works like this. a map file contains key=value
584 584 pairs. if value is quoted, it is treated as string. otherwise, it
585 585 is treated as name of template file.
586 586
587 587 templater is asked to expand a key in map. it looks up key, and
588 588 looks for strings like this: {foo}. it expands {foo} by looking up
589 589 foo in map, and substituting it. expansion is recursive: it stops
590 590 when there is no more {foo} to replace.
591 591
592 592 expansion also allows formatting and filtering.
593 593
594 594 format uses key to expand each item in list. syntax is
595 595 {key%format}.
596 596
597 597 filter uses function to transform value. syntax is
598 598 {key|filter1|filter2|...}.'''
599 599
600 600 def __init__(self, loader, filters=None, defaults=None, resources=None):
601 601 self._loader = loader
602 602 if filters is None:
603 603 filters = {}
604 604 self._filters = filters
605 605 self._funcs = templatefuncs.funcs # make this a parameter if needed
606 606 if defaults is None:
607 607 defaults = {}
608 608 if resources is None:
609 609 resources = nullresourcemapper()
610 610 self._defaults = defaults
611 611 self._resources = resources
612 612 self._cache = {} # key: (func, data)
613 613 self._tmplcache = {} # literal template: (func, data)
614 614
615 615 def overlaymap(self, origmapping, newmapping):
616 616 """Create combined mapping from the original mapping and partial
617 617 mapping to override the original"""
618 618 # do not copy symbols which overrides the defaults depending on
619 619 # new resources, so the defaults will be re-evaluated (issue5612)
620 620 knownres = self._resources.knownkeys()
621 newres = self._resources.availablekeys(self, newmapping)
621 newres = self._resources.availablekeys(newmapping)
622 622 mapping = {k: v for k, v in origmapping.iteritems()
623 623 if (k in knownres # not a symbol per self.symbol()
624 624 or newres.isdisjoint(self._defaultrequires(k)))}
625 625 mapping.update(newmapping)
626 626 mapping.update(
627 627 self._resources.populatemap(self, origmapping, newmapping))
628 628 return mapping
629 629
630 630 def _defaultrequires(self, key):
631 631 """Resource keys required by the specified default symbol function"""
632 632 v = self._defaults.get(key)
633 633 if v is None or not callable(v):
634 634 return ()
635 635 return getattr(v, '_requires', ())
636 636
637 637 def symbol(self, mapping, key):
638 638 """Resolve symbol to value or function; None if nothing found"""
639 639 v = None
640 640 if key not in self._resources.knownkeys():
641 641 v = mapping.get(key)
642 642 if v is None:
643 643 v = self._defaults.get(key)
644 644 return v
645 645
646 646 def availableresourcekeys(self, mapping):
647 647 """Return a set of available resource keys based on the given mapping"""
648 return self._resources.availablekeys(self, mapping)
648 return self._resources.availablekeys(mapping)
649 649
650 650 def knownresourcekeys(self):
651 651 """Return a set of supported resource keys"""
652 652 return self._resources.knownkeys()
653 653
654 654 def resource(self, mapping, key):
655 655 """Return internal data (e.g. cache) used for keyword/function
656 656 evaluation"""
657 v = self._resources.lookup(self, mapping, key)
657 v = self._resources.lookup(mapping, key)
658 658 if v is None:
659 659 raise templateutil.ResourceUnavailable(
660 660 _('template resource not available: %s') % key)
661 661 return v
662 662
663 663 def _load(self, t):
664 664 '''load, parse, and cache a template'''
665 665 if t not in self._cache:
666 666 x = self._loader(t)
667 667 # put poison to cut recursion while compiling 't'
668 668 self._cache[t] = (_runrecursivesymbol, t)
669 669 try:
670 670 self._cache[t] = compileexp(x, self, methods)
671 671 except: # re-raises
672 672 del self._cache[t]
673 673 raise
674 674 return self._cache[t]
675 675
676 676 def _parse(self, tmpl):
677 677 """Parse and cache a literal template"""
678 678 if tmpl not in self._tmplcache:
679 679 x = parse(tmpl)
680 680 self._tmplcache[tmpl] = compileexp(x, self, methods)
681 681 return self._tmplcache[tmpl]
682 682
683 683 def preload(self, t):
684 684 """Load, parse, and cache the specified template if available"""
685 685 try:
686 686 self._load(t)
687 687 return True
688 688 except templateutil.TemplateNotFound:
689 689 return False
690 690
691 691 def process(self, t, mapping):
692 692 '''Perform expansion. t is name of map element to expand.
693 693 mapping contains added elements for use during expansion. Is a
694 694 generator.'''
695 695 func, data = self._load(t)
696 696 return self._expand(func, data, mapping)
697 697
698 698 def expand(self, tmpl, mapping):
699 699 """Perform expansion over a literal template
700 700
701 701 No user aliases will be expanded since this is supposed to be called
702 702 with an internal template string.
703 703 """
704 704 func, data = self._parse(tmpl)
705 705 return self._expand(func, data, mapping)
706 706
707 707 def _expand(self, func, data, mapping):
708 708 # populate additional items only if they don't exist in the given
709 709 # mapping. this is slightly different from overlaymap() because the
710 710 # initial 'revcache' may contain pre-computed items.
711 711 extramapping = self._resources.populatemap(self, {}, mapping)
712 712 if extramapping:
713 713 extramapping.update(mapping)
714 714 mapping = extramapping
715 715 return templateutil.flatten(self, mapping, func(self, mapping, data))
716 716
717 717 def stylelist():
718 718 paths = templatepaths()
719 719 if not paths:
720 720 return _('no templates found, try `hg debuginstall` for more info')
721 721 dirlist = os.listdir(paths[0])
722 722 stylelist = []
723 723 for file in dirlist:
724 724 split = file.split(".")
725 725 if split[-1] in ('orig', 'rej'):
726 726 continue
727 727 if split[0] == "map-cmdline":
728 728 stylelist.append(split[1])
729 729 return ", ".join(sorted(stylelist))
730 730
731 731 def _readmapfile(mapfile):
732 732 """Load template elements from the given map file"""
733 733 if not os.path.exists(mapfile):
734 734 raise error.Abort(_("style '%s' not found") % mapfile,
735 735 hint=_("available styles: %s") % stylelist())
736 736
737 737 base = os.path.dirname(mapfile)
738 738 conf = config.config(includepaths=templatepaths())
739 739 conf.read(mapfile, remap={'': 'templates'})
740 740
741 741 cache = {}
742 742 tmap = {}
743 743 aliases = []
744 744
745 745 val = conf.get('templates', '__base__')
746 746 if val and val[0] not in "'\"":
747 747 # treat as a pointer to a base class for this style
748 748 path = util.normpath(os.path.join(base, val))
749 749
750 750 # fallback check in template paths
751 751 if not os.path.exists(path):
752 752 for p in templatepaths():
753 753 p2 = util.normpath(os.path.join(p, val))
754 754 if os.path.isfile(p2):
755 755 path = p2
756 756 break
757 757 p3 = util.normpath(os.path.join(p2, "map"))
758 758 if os.path.isfile(p3):
759 759 path = p3
760 760 break
761 761
762 762 cache, tmap, aliases = _readmapfile(path)
763 763
764 764 for key, val in conf['templates'].items():
765 765 if not val:
766 766 raise error.ParseError(_('missing value'),
767 767 conf.source('templates', key))
768 768 if val[0] in "'\"":
769 769 if val[0] != val[-1]:
770 770 raise error.ParseError(_('unmatched quotes'),
771 771 conf.source('templates', key))
772 772 cache[key] = unquotestring(val)
773 773 elif key != '__base__':
774 774 tmap[key] = os.path.join(base, val)
775 775 aliases.extend(conf['templatealias'].items())
776 776 return cache, tmap, aliases
777 777
778 778 class loader(object):
779 779 """Load template fragments optionally from a map file"""
780 780
781 781 def __init__(self, cache, aliases):
782 782 if cache is None:
783 783 cache = {}
784 784 self.cache = cache.copy()
785 785 self._map = {}
786 786 self._aliasmap = _aliasrules.buildmap(aliases)
787 787
788 788 def __contains__(self, key):
789 789 return key in self.cache or key in self._map
790 790
791 791 def load(self, t):
792 792 """Get parsed tree for the given template name. Use a local cache."""
793 793 if t not in self.cache:
794 794 try:
795 795 self.cache[t] = util.readfile(self._map[t])
796 796 except KeyError as inst:
797 797 raise templateutil.TemplateNotFound(
798 798 _('"%s" not in template map') % inst.args[0])
799 799 except IOError as inst:
800 800 reason = (_('template file %s: %s')
801 801 % (self._map[t],
802 802 stringutil.forcebytestr(inst.args[1])))
803 803 raise IOError(inst.args[0], encoding.strfromlocal(reason))
804 804 return self._parse(self.cache[t])
805 805
806 806 def _parse(self, tmpl):
807 807 x = parse(tmpl)
808 808 if self._aliasmap:
809 809 x = _aliasrules.expand(self._aliasmap, x)
810 810 return x
811 811
812 812 def _findsymbolsused(self, tree, syms):
813 813 if not tree:
814 814 return
815 815 op = tree[0]
816 816 if op == 'symbol':
817 817 s = tree[1]
818 818 if s in syms[0]:
819 819 return # avoid recursion: s -> cache[s] -> s
820 820 syms[0].add(s)
821 821 if s in self.cache or s in self._map:
822 822 # s may be a reference for named template
823 823 self._findsymbolsused(self.load(s), syms)
824 824 return
825 825 if op in {'integer', 'string'}:
826 826 return
827 827 # '{arg|func}' == '{func(arg)}'
828 828 if op == '|':
829 829 syms[1].add(getsymbol(tree[2]))
830 830 self._findsymbolsused(tree[1], syms)
831 831 return
832 832 if op == 'func':
833 833 syms[1].add(getsymbol(tree[1]))
834 834 self._findsymbolsused(tree[2], syms)
835 835 return
836 836 for x in tree[1:]:
837 837 self._findsymbolsused(x, syms)
838 838
839 839 def symbolsused(self, t):
840 840 """Look up (keywords, filters/functions) referenced from the name
841 841 template 't'
842 842
843 843 This may load additional templates from the map file.
844 844 """
845 845 syms = (set(), set())
846 846 self._findsymbolsused(self.load(t), syms)
847 847 return syms
848 848
849 849 class templater(object):
850 850
851 851 def __init__(self, filters=None, defaults=None, resources=None,
852 852 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
853 853 """Create template engine optionally with preloaded template fragments
854 854
855 855 - ``filters``: a dict of functions to transform a value into another.
856 856 - ``defaults``: a dict of symbol values/functions; may be overridden
857 857 by a ``mapping`` dict.
858 858 - ``resources``: a resourcemapper object to look up internal data
859 859 (e.g. cache), inaccessible from user template.
860 860 - ``cache``: a dict of preloaded template fragments.
861 861 - ``aliases``: a list of alias (name, replacement) pairs.
862 862
863 863 self.cache may be updated later to register additional template
864 864 fragments.
865 865 """
866 866 allfilters = templatefilters.filters.copy()
867 867 if filters:
868 868 allfilters.update(filters)
869 869 self._loader = loader(cache, aliases)
870 870 self._proc = engine(self._loader.load, allfilters, defaults, resources)
871 871 self._minchunk, self._maxchunk = minchunk, maxchunk
872 872
873 873 @classmethod
874 874 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
875 875 cache=None, minchunk=1024, maxchunk=65536):
876 876 """Create templater from the specified map file"""
877 877 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
878 878 cache, tmap, aliases = _readmapfile(mapfile)
879 879 t._loader.cache.update(cache)
880 880 t._loader._map = tmap
881 881 t._loader._aliasmap = _aliasrules.buildmap(aliases)
882 882 return t
883 883
884 884 def __contains__(self, key):
885 885 return key in self._loader
886 886
887 887 @property
888 888 def cache(self):
889 889 return self._loader.cache
890 890
891 891 # for highlight extension to insert one-time 'colorize' filter
892 892 @property
893 893 def _filters(self):
894 894 return self._proc._filters
895 895
896 896 @property
897 897 def defaults(self):
898 898 return self._proc._defaults
899 899
900 900 def load(self, t):
901 901 """Get parsed tree for the given template name. Use a local cache."""
902 902 return self._loader.load(t)
903 903
904 904 def symbolsuseddefault(self):
905 905 """Look up (keywords, filters/functions) referenced from the default
906 906 unnamed template
907 907
908 908 This may load additional templates from the map file.
909 909 """
910 910 return self.symbolsused('')
911 911
912 912 def symbolsused(self, t):
913 913 """Look up (keywords, filters/functions) referenced from the name
914 914 template 't'
915 915
916 916 This may load additional templates from the map file.
917 917 """
918 918 return self._loader.symbolsused(t)
919 919
920 920 def renderdefault(self, mapping):
921 921 """Render the default unnamed template and return result as string"""
922 922 return self.render('', mapping)
923 923
924 924 def render(self, t, mapping):
925 925 """Render the specified named template and return result as string"""
926 926 return b''.join(self.generate(t, mapping))
927 927
928 928 def generate(self, t, mapping):
929 929 """Return a generator that renders the specified named template and
930 930 yields chunks"""
931 931 stream = self._proc.process(t, mapping)
932 932 if self._minchunk:
933 933 stream = util.increasingchunks(stream, min=self._minchunk,
934 934 max=self._maxchunk)
935 935 return stream
936 936
937 937 def templatepaths():
938 938 '''return locations used for template files.'''
939 939 pathsrel = ['templates']
940 940 paths = [os.path.normpath(os.path.join(util.datapath, f))
941 941 for f in pathsrel]
942 942 return [p for p in paths if os.path.isdir(p)]
943 943
944 944 def templatepath(name):
945 945 '''return location of template file. returns None if not found.'''
946 946 for p in templatepaths():
947 947 f = os.path.join(p, name)
948 948 if os.path.exists(f):
949 949 return f
950 950 return None
951 951
952 952 def stylemap(styles, paths=None):
953 953 """Return path to mapfile for a given style.
954 954
955 955 Searches mapfile in the following locations:
956 956 1. templatepath/style/map
957 957 2. templatepath/map-style
958 958 3. templatepath/map
959 959 """
960 960
961 961 if paths is None:
962 962 paths = templatepaths()
963 963 elif isinstance(paths, bytes):
964 964 paths = [paths]
965 965
966 966 if isinstance(styles, bytes):
967 967 styles = [styles]
968 968
969 969 for style in styles:
970 970 # only plain name is allowed to honor template paths
971 971 if (not style
972 972 or style in (pycompat.oscurdir, pycompat.ospardir)
973 973 or pycompat.ossep in style
974 974 or pycompat.osaltsep and pycompat.osaltsep in style):
975 975 continue
976 976 locations = [os.path.join(style, 'map'), 'map-' + style]
977 977 locations.append('map')
978 978
979 979 for path in paths:
980 980 for location in locations:
981 981 mapfile = os.path.join(path, location)
982 982 if os.path.isfile(mapfile):
983 983 return style, mapfile
984 984
985 985 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,963 +1,963 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 abc
11 11 import types
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 util,
18 18 )
19 19 from .utils import (
20 20 dateutil,
21 21 stringutil,
22 22 )
23 23
24 24 class ResourceUnavailable(error.Abort):
25 25 pass
26 26
27 27 class TemplateNotFound(error.Abort):
28 28 pass
29 29
30 30 class wrapped(object):
31 31 """Object requiring extra conversion prior to displaying or processing
32 32 as value
33 33
34 34 Use unwrapvalue() or unwrapastype() to obtain the inner object.
35 35 """
36 36
37 37 __metaclass__ = abc.ABCMeta
38 38
39 39 @abc.abstractmethod
40 40 def contains(self, context, mapping, item):
41 41 """Test if the specified item is in self
42 42
43 43 The item argument may be a wrapped object.
44 44 """
45 45
46 46 @abc.abstractmethod
47 47 def getmember(self, context, mapping, key):
48 48 """Return a member item for the specified key
49 49
50 50 The key argument may be a wrapped object.
51 51 A returned object may be either a wrapped object or a pure value
52 52 depending on the self type.
53 53 """
54 54
55 55 @abc.abstractmethod
56 56 def getmin(self, context, mapping):
57 57 """Return the smallest item, which may be either a wrapped or a pure
58 58 value depending on the self type"""
59 59
60 60 @abc.abstractmethod
61 61 def getmax(self, context, mapping):
62 62 """Return the largest item, which may be either a wrapped or a pure
63 63 value depending on the self type"""
64 64
65 65 @abc.abstractmethod
66 66 def filter(self, context, mapping, select):
67 67 """Return new container of the same type which includes only the
68 68 selected elements
69 69
70 70 select() takes each item as a wrapped object and returns True/False.
71 71 """
72 72
73 73 @abc.abstractmethod
74 74 def itermaps(self, context):
75 75 """Yield each template mapping"""
76 76
77 77 @abc.abstractmethod
78 78 def join(self, context, mapping, sep):
79 79 """Join items with the separator; Returns a bytes or (possibly nested)
80 80 generator of bytes
81 81
82 82 A pre-configured template may be rendered per item if this container
83 83 holds unprintable items.
84 84 """
85 85
86 86 @abc.abstractmethod
87 87 def show(self, context, mapping):
88 88 """Return a bytes or (possibly nested) generator of bytes representing
89 89 the underlying object
90 90
91 91 A pre-configured template may be rendered if the underlying object is
92 92 not printable.
93 93 """
94 94
95 95 @abc.abstractmethod
96 96 def tobool(self, context, mapping):
97 97 """Return a boolean representation of the inner value"""
98 98
99 99 @abc.abstractmethod
100 100 def tovalue(self, context, mapping):
101 101 """Move the inner value object out or create a value representation
102 102
103 103 A returned value must be serializable by templaterfilters.json().
104 104 """
105 105
106 106 class mappable(object):
107 107 """Object which can be converted to a single template mapping"""
108 108
109 109 def itermaps(self, context):
110 110 yield self.tomap(context)
111 111
112 112 @abc.abstractmethod
113 113 def tomap(self, context):
114 114 """Create a single template mapping representing this"""
115 115
116 116 class wrappedbytes(wrapped):
117 117 """Wrapper for byte string"""
118 118
119 119 def __init__(self, value):
120 120 self._value = value
121 121
122 122 def contains(self, context, mapping, item):
123 123 item = stringify(context, mapping, item)
124 124 return item in self._value
125 125
126 126 def getmember(self, context, mapping, key):
127 127 raise error.ParseError(_('%r is not a dictionary')
128 128 % pycompat.bytestr(self._value))
129 129
130 130 def getmin(self, context, mapping):
131 131 return self._getby(context, mapping, min)
132 132
133 133 def getmax(self, context, mapping):
134 134 return self._getby(context, mapping, max)
135 135
136 136 def _getby(self, context, mapping, func):
137 137 if not self._value:
138 138 raise error.ParseError(_('empty string'))
139 139 return func(pycompat.iterbytestr(self._value))
140 140
141 141 def filter(self, context, mapping, select):
142 142 raise error.ParseError(_('%r is not filterable')
143 143 % pycompat.bytestr(self._value))
144 144
145 145 def itermaps(self, context):
146 146 raise error.ParseError(_('%r is not iterable of mappings')
147 147 % pycompat.bytestr(self._value))
148 148
149 149 def join(self, context, mapping, sep):
150 150 return joinitems(pycompat.iterbytestr(self._value), sep)
151 151
152 152 def show(self, context, mapping):
153 153 return self._value
154 154
155 155 def tobool(self, context, mapping):
156 156 return bool(self._value)
157 157
158 158 def tovalue(self, context, mapping):
159 159 return self._value
160 160
161 161 class wrappedvalue(wrapped):
162 162 """Generic wrapper for pure non-list/dict/bytes value"""
163 163
164 164 def __init__(self, value):
165 165 self._value = value
166 166
167 167 def contains(self, context, mapping, item):
168 168 raise error.ParseError(_("%r is not iterable") % self._value)
169 169
170 170 def getmember(self, context, mapping, key):
171 171 raise error.ParseError(_('%r is not a dictionary') % self._value)
172 172
173 173 def getmin(self, context, mapping):
174 174 raise error.ParseError(_("%r is not iterable") % self._value)
175 175
176 176 def getmax(self, context, mapping):
177 177 raise error.ParseError(_("%r is not iterable") % self._value)
178 178
179 179 def filter(self, context, mapping, select):
180 180 raise error.ParseError(_("%r is not iterable") % self._value)
181 181
182 182 def itermaps(self, context):
183 183 raise error.ParseError(_('%r is not iterable of mappings')
184 184 % self._value)
185 185
186 186 def join(self, context, mapping, sep):
187 187 raise error.ParseError(_('%r is not iterable') % self._value)
188 188
189 189 def show(self, context, mapping):
190 190 if self._value is None:
191 191 return b''
192 192 return pycompat.bytestr(self._value)
193 193
194 194 def tobool(self, context, mapping):
195 195 if self._value is None:
196 196 return False
197 197 if isinstance(self._value, bool):
198 198 return self._value
199 199 # otherwise evaluate as string, which means 0 is True
200 200 return bool(pycompat.bytestr(self._value))
201 201
202 202 def tovalue(self, context, mapping):
203 203 return self._value
204 204
205 205 class date(mappable, wrapped):
206 206 """Wrapper for date tuple"""
207 207
208 208 def __init__(self, value, showfmt='%d %d'):
209 209 # value may be (float, int), but public interface shouldn't support
210 210 # floating-point timestamp
211 211 self._unixtime, self._tzoffset = map(int, value)
212 212 self._showfmt = showfmt
213 213
214 214 def contains(self, context, mapping, item):
215 215 raise error.ParseError(_('date is not iterable'))
216 216
217 217 def getmember(self, context, mapping, key):
218 218 raise error.ParseError(_('date is not a dictionary'))
219 219
220 220 def getmin(self, context, mapping):
221 221 raise error.ParseError(_('date is not iterable'))
222 222
223 223 def getmax(self, context, mapping):
224 224 raise error.ParseError(_('date is not iterable'))
225 225
226 226 def filter(self, context, mapping, select):
227 227 raise error.ParseError(_('date is not iterable'))
228 228
229 229 def join(self, context, mapping, sep):
230 230 raise error.ParseError(_("date is not iterable"))
231 231
232 232 def show(self, context, mapping):
233 233 return self._showfmt % (self._unixtime, self._tzoffset)
234 234
235 235 def tomap(self, context):
236 236 return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
237 237
238 238 def tobool(self, context, mapping):
239 239 return True
240 240
241 241 def tovalue(self, context, mapping):
242 242 return (self._unixtime, self._tzoffset)
243 243
244 244 class hybrid(wrapped):
245 245 """Wrapper for list or dict to support legacy template
246 246
247 247 This class allows us to handle both:
248 248 - "{files}" (legacy command-line-specific list hack) and
249 249 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
250 250 and to access raw values:
251 251 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
252 252 - "{get(extras, key)}"
253 253 - "{files|json}"
254 254 """
255 255
256 256 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
257 257 self._gen = gen # generator or function returning generator
258 258 self._values = values
259 259 self._makemap = makemap
260 260 self._joinfmt = joinfmt
261 261 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
262 262
263 263 def contains(self, context, mapping, item):
264 264 item = unwrapastype(context, mapping, item, self._keytype)
265 265 return item in self._values
266 266
267 267 def getmember(self, context, mapping, key):
268 268 # TODO: maybe split hybrid list/dict types?
269 269 if not util.safehasattr(self._values, 'get'):
270 270 raise error.ParseError(_('not a dictionary'))
271 271 key = unwrapastype(context, mapping, key, self._keytype)
272 272 return self._wrapvalue(key, self._values.get(key))
273 273
274 274 def getmin(self, context, mapping):
275 275 return self._getby(context, mapping, min)
276 276
277 277 def getmax(self, context, mapping):
278 278 return self._getby(context, mapping, max)
279 279
280 280 def _getby(self, context, mapping, func):
281 281 if not self._values:
282 282 raise error.ParseError(_('empty sequence'))
283 283 val = func(self._values)
284 284 return self._wrapvalue(val, val)
285 285
286 286 def _wrapvalue(self, key, val):
287 287 if val is None:
288 288 return
289 289 if util.safehasattr(val, '_makemap'):
290 290 # a nested hybrid list/dict, which has its own way of map operation
291 291 return val
292 292 return hybriditem(None, key, val, self._makemap)
293 293
294 294 def filter(self, context, mapping, select):
295 295 if util.safehasattr(self._values, 'get'):
296 296 values = {k: v for k, v in self._values.iteritems()
297 297 if select(self._wrapvalue(k, v))}
298 298 else:
299 299 values = [v for v in self._values if select(self._wrapvalue(v, v))]
300 300 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
301 301
302 302 def itermaps(self, context):
303 303 makemap = self._makemap
304 304 for x in self._values:
305 305 yield makemap(x)
306 306
307 307 def join(self, context, mapping, sep):
308 308 # TODO: switch gen to (context, mapping) API?
309 309 return joinitems((self._joinfmt(x) for x in self._values), sep)
310 310
311 311 def show(self, context, mapping):
312 312 # TODO: switch gen to (context, mapping) API?
313 313 gen = self._gen
314 314 if gen is None:
315 315 return self.join(context, mapping, ' ')
316 316 if callable(gen):
317 317 return gen()
318 318 return gen
319 319
320 320 def tobool(self, context, mapping):
321 321 return bool(self._values)
322 322
323 323 def tovalue(self, context, mapping):
324 324 # TODO: make it non-recursive for trivial lists/dicts
325 325 xs = self._values
326 326 if util.safehasattr(xs, 'get'):
327 327 return {k: unwrapvalue(context, mapping, v)
328 328 for k, v in xs.iteritems()}
329 329 return [unwrapvalue(context, mapping, x) for x in xs]
330 330
331 331 class hybriditem(mappable, wrapped):
332 332 """Wrapper for non-list/dict object to support map operation
333 333
334 334 This class allows us to handle both:
335 335 - "{manifest}"
336 336 - "{manifest % '{rev}:{node}'}"
337 337 - "{manifest.rev}"
338 338 """
339 339
340 340 def __init__(self, gen, key, value, makemap):
341 341 self._gen = gen # generator or function returning generator
342 342 self._key = key
343 343 self._value = value # may be generator of strings
344 344 self._makemap = makemap
345 345
346 346 def tomap(self, context):
347 347 return self._makemap(self._key)
348 348
349 349 def contains(self, context, mapping, item):
350 350 w = makewrapped(context, mapping, self._value)
351 351 return w.contains(context, mapping, item)
352 352
353 353 def getmember(self, context, mapping, key):
354 354 w = makewrapped(context, mapping, self._value)
355 355 return w.getmember(context, mapping, key)
356 356
357 357 def getmin(self, context, mapping):
358 358 w = makewrapped(context, mapping, self._value)
359 359 return w.getmin(context, mapping)
360 360
361 361 def getmax(self, context, mapping):
362 362 w = makewrapped(context, mapping, self._value)
363 363 return w.getmax(context, mapping)
364 364
365 365 def filter(self, context, mapping, select):
366 366 w = makewrapped(context, mapping, self._value)
367 367 return w.filter(context, mapping, select)
368 368
369 369 def join(self, context, mapping, sep):
370 370 w = makewrapped(context, mapping, self._value)
371 371 return w.join(context, mapping, sep)
372 372
373 373 def show(self, context, mapping):
374 374 # TODO: switch gen to (context, mapping) API?
375 375 gen = self._gen
376 376 if gen is None:
377 377 return pycompat.bytestr(self._value)
378 378 if callable(gen):
379 379 return gen()
380 380 return gen
381 381
382 382 def tobool(self, context, mapping):
383 383 w = makewrapped(context, mapping, self._value)
384 384 return w.tobool(context, mapping)
385 385
386 386 def tovalue(self, context, mapping):
387 387 return _unthunk(context, mapping, self._value)
388 388
389 389 class _mappingsequence(wrapped):
390 390 """Wrapper for sequence of template mappings
391 391
392 392 This represents an inner template structure (i.e. a list of dicts),
393 393 which can also be rendered by the specified named/literal template.
394 394
395 395 Template mappings may be nested.
396 396 """
397 397
398 398 def __init__(self, name=None, tmpl=None, sep=''):
399 399 if name is not None and tmpl is not None:
400 400 raise error.ProgrammingError('name and tmpl are mutually exclusive')
401 401 self._name = name
402 402 self._tmpl = tmpl
403 403 self._defaultsep = sep
404 404
405 405 def contains(self, context, mapping, item):
406 406 raise error.ParseError(_('not comparable'))
407 407
408 408 def getmember(self, context, mapping, key):
409 409 raise error.ParseError(_('not a dictionary'))
410 410
411 411 def getmin(self, context, mapping):
412 412 raise error.ParseError(_('not comparable'))
413 413
414 414 def getmax(self, context, mapping):
415 415 raise error.ParseError(_('not comparable'))
416 416
417 417 def filter(self, context, mapping, select):
418 418 # implement if necessary; we'll need a wrapped type for a mapping dict
419 419 raise error.ParseError(_('not filterable without template'))
420 420
421 421 def join(self, context, mapping, sep):
422 422 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
423 423 if self._name:
424 424 itemiter = (context.process(self._name, m) for m in mapsiter)
425 425 elif self._tmpl:
426 426 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
427 427 else:
428 428 raise error.ParseError(_('not displayable without template'))
429 429 return joinitems(itemiter, sep)
430 430
431 431 def show(self, context, mapping):
432 432 return self.join(context, mapping, self._defaultsep)
433 433
434 434 def tovalue(self, context, mapping):
435 435 knownres = context.knownresourcekeys()
436 436 items = []
437 437 for nm in self.itermaps(context):
438 438 # drop internal resources (recursively) which shouldn't be displayed
439 439 lm = context.overlaymap(mapping, nm)
440 440 items.append({k: unwrapvalue(context, lm, v)
441 441 for k, v in nm.iteritems() if k not in knownres})
442 442 return items
443 443
444 444 class mappinggenerator(_mappingsequence):
445 445 """Wrapper for generator of template mappings
446 446
447 447 The function ``make(context, *args)`` should return a generator of
448 448 mapping dicts.
449 449 """
450 450
451 451 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
452 452 super(mappinggenerator, self).__init__(name, tmpl, sep)
453 453 self._make = make
454 454 self._args = args
455 455
456 456 def itermaps(self, context):
457 457 return self._make(context, *self._args)
458 458
459 459 def tobool(self, context, mapping):
460 460 return _nonempty(self.itermaps(context))
461 461
462 462 class mappinglist(_mappingsequence):
463 463 """Wrapper for list of template mappings"""
464 464
465 465 def __init__(self, mappings, name=None, tmpl=None, sep=''):
466 466 super(mappinglist, self).__init__(name, tmpl, sep)
467 467 self._mappings = mappings
468 468
469 469 def itermaps(self, context):
470 470 return iter(self._mappings)
471 471
472 472 def tobool(self, context, mapping):
473 473 return bool(self._mappings)
474 474
475 475 class mappedgenerator(wrapped):
476 476 """Wrapper for generator of strings which acts as a list
477 477
478 478 The function ``make(context, *args)`` should return a generator of
479 479 byte strings, or a generator of (possibly nested) generators of byte
480 480 strings (i.e. a generator for a list of byte strings.)
481 481 """
482 482
483 483 def __init__(self, make, args=()):
484 484 self._make = make
485 485 self._args = args
486 486
487 487 def contains(self, context, mapping, item):
488 488 item = stringify(context, mapping, item)
489 489 return item in self.tovalue(context, mapping)
490 490
491 491 def _gen(self, context):
492 492 return self._make(context, *self._args)
493 493
494 494 def getmember(self, context, mapping, key):
495 495 raise error.ParseError(_('not a dictionary'))
496 496
497 497 def getmin(self, context, mapping):
498 498 return self._getby(context, mapping, min)
499 499
500 500 def getmax(self, context, mapping):
501 501 return self._getby(context, mapping, max)
502 502
503 503 def _getby(self, context, mapping, func):
504 504 xs = self.tovalue(context, mapping)
505 505 if not xs:
506 506 raise error.ParseError(_('empty sequence'))
507 507 return func(xs)
508 508
509 509 @staticmethod
510 510 def _filteredgen(context, mapping, make, args, select):
511 511 for x in make(context, *args):
512 512 s = stringify(context, mapping, x)
513 513 if select(wrappedbytes(s)):
514 514 yield s
515 515
516 516 def filter(self, context, mapping, select):
517 517 args = (mapping, self._make, self._args, select)
518 518 return mappedgenerator(self._filteredgen, args)
519 519
520 520 def itermaps(self, context):
521 521 raise error.ParseError(_('list of strings is not mappable'))
522 522
523 523 def join(self, context, mapping, sep):
524 524 return joinitems(self._gen(context), sep)
525 525
526 526 def show(self, context, mapping):
527 527 return self.join(context, mapping, '')
528 528
529 529 def tobool(self, context, mapping):
530 530 return _nonempty(self._gen(context))
531 531
532 532 def tovalue(self, context, mapping):
533 533 return [stringify(context, mapping, x) for x in self._gen(context)]
534 534
535 535 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
536 536 """Wrap data to support both dict-like and string-like operations"""
537 537 prefmt = pycompat.identity
538 538 if fmt is None:
539 539 fmt = '%s=%s'
540 540 prefmt = pycompat.bytestr
541 541 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
542 542 lambda k: fmt % (prefmt(k), prefmt(data[k])))
543 543
544 544 def hybridlist(data, name, fmt=None, gen=None):
545 545 """Wrap data to support both list-like and string-like operations"""
546 546 prefmt = pycompat.identity
547 547 if fmt is None:
548 548 fmt = '%s'
549 549 prefmt = pycompat.bytestr
550 550 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
551 551
552 552 def compatdict(context, mapping, name, data, key='key', value='value',
553 553 fmt=None, plural=None, separator=' '):
554 554 """Wrap data like hybriddict(), but also supports old-style list template
555 555
556 556 This exists for backward compatibility with the old-style template. Use
557 557 hybriddict() for new template keywords.
558 558 """
559 559 c = [{key: k, value: v} for k, v in data.iteritems()]
560 560 f = _showcompatlist(context, mapping, name, c, plural, separator)
561 561 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
562 562
563 563 def compatlist(context, mapping, name, data, element=None, fmt=None,
564 564 plural=None, separator=' '):
565 565 """Wrap data like hybridlist(), but also supports old-style list template
566 566
567 567 This exists for backward compatibility with the old-style template. Use
568 568 hybridlist() for new template keywords.
569 569 """
570 570 f = _showcompatlist(context, mapping, name, data, plural, separator)
571 571 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
572 572
573 573 def compatfilecopiesdict(context, mapping, name, copies):
574 574 """Wrap list of (dest, source) file names to support old-style list
575 575 template and field names
576 576
577 577 This exists for backward compatibility. Use hybriddict for new template
578 578 keywords.
579 579 """
580 580 # no need to provide {path} to old-style list template
581 581 c = [{'name': k, 'source': v} for k, v in copies]
582 582 f = _showcompatlist(context, mapping, name, c, plural='file_copies')
583 583 copies = util.sortdict(copies)
584 584 return hybrid(f, copies,
585 585 lambda k: {'name': k, 'path': k, 'source': copies[k]},
586 586 lambda k: '%s (%s)' % (k, copies[k]))
587 587
588 588 def compatfileslist(context, mapping, name, files):
589 589 """Wrap list of file names to support old-style list template and field
590 590 names
591 591
592 592 This exists for backward compatibility. Use hybridlist for new template
593 593 keywords.
594 594 """
595 595 f = _showcompatlist(context, mapping, name, files)
596 596 return hybrid(f, files, lambda x: {'file': x, 'path': x},
597 597 pycompat.identity)
598 598
599 599 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
600 600 """Return a generator that renders old-style list template
601 601
602 602 name is name of key in template map.
603 603 values is list of strings or dicts.
604 604 plural is plural of name, if not simply name + 's'.
605 605 separator is used to join values as a string
606 606
607 607 expansion works like this, given name 'foo'.
608 608
609 609 if values is empty, expand 'no_foos'.
610 610
611 611 if 'foo' not in template map, return values as a string,
612 612 joined by 'separator'.
613 613
614 614 expand 'start_foos'.
615 615
616 616 for each value, expand 'foo'. if 'last_foo' in template
617 617 map, expand it instead of 'foo' for last key.
618 618
619 619 expand 'end_foos'.
620 620 """
621 621 if not plural:
622 622 plural = name + 's'
623 623 if not values:
624 624 noname = 'no_' + plural
625 625 if context.preload(noname):
626 626 yield context.process(noname, mapping)
627 627 return
628 628 if not context.preload(name):
629 629 if isinstance(values[0], bytes):
630 630 yield separator.join(values)
631 631 else:
632 632 for v in values:
633 633 r = dict(v)
634 634 r.update(mapping)
635 635 yield r
636 636 return
637 637 startname = 'start_' + plural
638 638 if context.preload(startname):
639 639 yield context.process(startname, mapping)
640 640 def one(v, tag=name):
641 641 vmapping = {}
642 642 try:
643 643 vmapping.update(v)
644 644 # Python 2 raises ValueError if the type of v is wrong. Python
645 645 # 3 raises TypeError.
646 646 except (AttributeError, TypeError, ValueError):
647 647 try:
648 648 # Python 2 raises ValueError trying to destructure an e.g.
649 649 # bytes. Python 3 raises TypeError.
650 650 for a, b in v:
651 651 vmapping[a] = b
652 652 except (TypeError, ValueError):
653 653 vmapping[name] = v
654 654 vmapping = context.overlaymap(mapping, vmapping)
655 655 return context.process(tag, vmapping)
656 656 lastname = 'last_' + name
657 657 if context.preload(lastname):
658 658 last = values.pop()
659 659 else:
660 660 last = None
661 661 for v in values:
662 662 yield one(v)
663 663 if last is not None:
664 664 yield one(last, tag=lastname)
665 665 endname = 'end_' + plural
666 666 if context.preload(endname):
667 667 yield context.process(endname, mapping)
668 668
669 669 def flatten(context, mapping, thing):
670 670 """Yield a single stream from a possibly nested set of iterators"""
671 671 if isinstance(thing, wrapped):
672 672 thing = thing.show(context, mapping)
673 673 if isinstance(thing, bytes):
674 674 yield thing
675 675 elif isinstance(thing, str):
676 676 # We can only hit this on Python 3, and it's here to guard
677 677 # against infinite recursion.
678 678 raise error.ProgrammingError('Mercurial IO including templates is done'
679 679 ' with bytes, not strings, got %r' % thing)
680 680 elif thing is None:
681 681 pass
682 682 elif not util.safehasattr(thing, '__iter__'):
683 683 yield pycompat.bytestr(thing)
684 684 else:
685 685 for i in thing:
686 686 if isinstance(i, wrapped):
687 687 i = i.show(context, mapping)
688 688 if isinstance(i, bytes):
689 689 yield i
690 690 elif i is None:
691 691 pass
692 692 elif not util.safehasattr(i, '__iter__'):
693 693 yield pycompat.bytestr(i)
694 694 else:
695 695 for j in flatten(context, mapping, i):
696 696 yield j
697 697
698 698 def stringify(context, mapping, thing):
699 699 """Turn values into bytes by converting into text and concatenating them"""
700 700 if isinstance(thing, bytes):
701 701 return thing # retain localstr to be round-tripped
702 702 return b''.join(flatten(context, mapping, thing))
703 703
704 704 def findsymbolicname(arg):
705 705 """Find symbolic name for the given compiled expression; returns None
706 706 if nothing found reliably"""
707 707 while True:
708 708 func, data = arg
709 709 if func is runsymbol:
710 710 return data
711 711 elif func is runfilter:
712 712 arg = data[0]
713 713 else:
714 714 return None
715 715
716 716 def _nonempty(xiter):
717 717 try:
718 718 next(xiter)
719 719 return True
720 720 except StopIteration:
721 721 return False
722 722
723 723 def _unthunk(context, mapping, thing):
724 724 """Evaluate a lazy byte string into value"""
725 725 if not isinstance(thing, types.GeneratorType):
726 726 return thing
727 727 return stringify(context, mapping, thing)
728 728
729 729 def evalrawexp(context, mapping, arg):
730 730 """Evaluate given argument as a bare template object which may require
731 731 further processing (such as folding generator of strings)"""
732 732 func, data = arg
733 733 return func(context, mapping, data)
734 734
735 735 def evalwrapped(context, mapping, arg):
736 736 """Evaluate given argument to wrapped object"""
737 737 thing = evalrawexp(context, mapping, arg)
738 738 return makewrapped(context, mapping, thing)
739 739
740 740 def makewrapped(context, mapping, thing):
741 741 """Lift object to a wrapped type"""
742 742 if isinstance(thing, wrapped):
743 743 return thing
744 744 thing = _unthunk(context, mapping, thing)
745 745 if isinstance(thing, bytes):
746 746 return wrappedbytes(thing)
747 747 return wrappedvalue(thing)
748 748
749 749 def evalfuncarg(context, mapping, arg):
750 750 """Evaluate given argument as value type"""
751 751 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
752 752
753 753 def unwrapvalue(context, mapping, thing):
754 754 """Move the inner value object out of the wrapper"""
755 755 if isinstance(thing, wrapped):
756 756 return thing.tovalue(context, mapping)
757 757 # evalrawexp() may return string, generator of strings or arbitrary object
758 758 # such as date tuple, but filter does not want generator.
759 759 return _unthunk(context, mapping, thing)
760 760
761 761 def evalboolean(context, mapping, arg):
762 762 """Evaluate given argument as boolean, but also takes boolean literals"""
763 763 func, data = arg
764 764 if func is runsymbol:
765 765 thing = func(context, mapping, data, default=None)
766 766 if thing is None:
767 767 # not a template keyword, takes as a boolean literal
768 768 thing = stringutil.parsebool(data)
769 769 else:
770 770 thing = func(context, mapping, data)
771 771 return makewrapped(context, mapping, thing).tobool(context, mapping)
772 772
773 773 def evaldate(context, mapping, arg, err=None):
774 774 """Evaluate given argument as a date tuple or a date string; returns
775 775 a (unixtime, offset) tuple"""
776 776 thing = evalrawexp(context, mapping, arg)
777 777 return unwrapdate(context, mapping, thing, err)
778 778
779 779 def unwrapdate(context, mapping, thing, err=None):
780 780 if isinstance(thing, date):
781 781 return thing.tovalue(context, mapping)
782 782 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
783 783 thing = unwrapvalue(context, mapping, thing)
784 784 try:
785 785 return dateutil.parsedate(thing)
786 786 except AttributeError:
787 787 raise error.ParseError(err or _('not a date tuple nor a string'))
788 788 except error.ParseError:
789 789 if not err:
790 790 raise
791 791 raise error.ParseError(err)
792 792
793 793 def evalinteger(context, mapping, arg, err=None):
794 794 thing = evalrawexp(context, mapping, arg)
795 795 return unwrapinteger(context, mapping, thing, err)
796 796
797 797 def unwrapinteger(context, mapping, thing, err=None):
798 798 thing = unwrapvalue(context, mapping, thing)
799 799 try:
800 800 return int(thing)
801 801 except (TypeError, ValueError):
802 802 raise error.ParseError(err or _('not an integer'))
803 803
804 804 def evalstring(context, mapping, arg):
805 805 return stringify(context, mapping, evalrawexp(context, mapping, arg))
806 806
807 807 def evalstringliteral(context, mapping, arg):
808 808 """Evaluate given argument as string template, but returns symbol name
809 809 if it is unknown"""
810 810 func, data = arg
811 811 if func is runsymbol:
812 812 thing = func(context, mapping, data, default=data)
813 813 else:
814 814 thing = func(context, mapping, data)
815 815 return stringify(context, mapping, thing)
816 816
817 817 _unwrapfuncbytype = {
818 818 None: unwrapvalue,
819 819 bytes: stringify,
820 820 date: unwrapdate,
821 821 int: unwrapinteger,
822 822 }
823 823
824 824 def unwrapastype(context, mapping, thing, typ):
825 825 """Move the inner value object out of the wrapper and coerce its type"""
826 826 try:
827 827 f = _unwrapfuncbytype[typ]
828 828 except KeyError:
829 829 raise error.ProgrammingError('invalid type specified: %r' % typ)
830 830 return f(context, mapping, thing)
831 831
832 832 def runinteger(context, mapping, data):
833 833 return int(data)
834 834
835 835 def runstring(context, mapping, data):
836 836 return data
837 837
838 838 def _recursivesymbolblocker(key):
839 839 def showrecursion(context, mapping):
840 840 raise error.Abort(_("recursive reference '%s' in template") % key)
841 841 showrecursion._requires = () # mark as new-style templatekw
842 842 return showrecursion
843 843
844 844 def runsymbol(context, mapping, key, default=''):
845 845 v = context.symbol(mapping, key)
846 846 if v is None:
847 847 # put poison to cut recursion. we can't move this to parsing phase
848 848 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
849 849 safemapping = mapping.copy()
850 850 safemapping[key] = _recursivesymbolblocker(key)
851 851 try:
852 852 v = context.process(key, safemapping)
853 853 except TemplateNotFound:
854 854 v = default
855 855 if callable(v) and getattr(v, '_requires', None) is None:
856 856 # old templatekw: expand all keywords and resources
857 857 # (TODO: drop support for old-style functions. 'f._requires = ()'
858 858 # can be removed.)
859 props = {k: context._resources.lookup(context, mapping, k)
859 props = {k: context._resources.lookup(mapping, k)
860 860 for k in context._resources.knownkeys()}
861 861 # pass context to _showcompatlist() through templatekw._showlist()
862 862 props['templ'] = context
863 863 props.update(mapping)
864 864 ui = props.get('ui')
865 865 if ui:
866 866 ui.deprecwarn("old-style template keyword '%s'" % key, '4.8')
867 867 return v(**pycompat.strkwargs(props))
868 868 if callable(v):
869 869 # new templatekw
870 870 try:
871 871 return v(context, mapping)
872 872 except ResourceUnavailable:
873 873 # unsupported keyword is mapped to empty just like unknown keyword
874 874 return None
875 875 return v
876 876
877 877 def runtemplate(context, mapping, template):
878 878 for arg in template:
879 879 yield evalrawexp(context, mapping, arg)
880 880
881 881 def runfilter(context, mapping, data):
882 882 arg, filt = data
883 883 thing = evalrawexp(context, mapping, arg)
884 884 intype = getattr(filt, '_intype', None)
885 885 try:
886 886 thing = unwrapastype(context, mapping, thing, intype)
887 887 return filt(thing)
888 888 except error.ParseError as e:
889 889 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
890 890
891 891 def _formatfiltererror(arg, filt):
892 892 fn = pycompat.sysbytes(filt.__name__)
893 893 sym = findsymbolicname(arg)
894 894 if not sym:
895 895 return _("incompatible use of template filter '%s'") % fn
896 896 return (_("template filter '%s' is not compatible with keyword '%s'")
897 897 % (fn, sym))
898 898
899 899 def _iteroverlaymaps(context, origmapping, newmappings):
900 900 """Generate combined mappings from the original mapping and an iterable
901 901 of partial mappings to override the original"""
902 902 for i, nm in enumerate(newmappings):
903 903 lm = context.overlaymap(origmapping, nm)
904 904 lm['index'] = i
905 905 yield lm
906 906
907 907 def _applymap(context, mapping, d, darg, targ):
908 908 try:
909 909 diter = d.itermaps(context)
910 910 except error.ParseError as err:
911 911 sym = findsymbolicname(darg)
912 912 if not sym:
913 913 raise
914 914 hint = _("keyword '%s' does not support map operation") % sym
915 915 raise error.ParseError(bytes(err), hint=hint)
916 916 for lm in _iteroverlaymaps(context, mapping, diter):
917 917 yield evalrawexp(context, lm, targ)
918 918
919 919 def runmap(context, mapping, data):
920 920 darg, targ = data
921 921 d = evalwrapped(context, mapping, darg)
922 922 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
923 923
924 924 def runmember(context, mapping, data):
925 925 darg, memb = data
926 926 d = evalwrapped(context, mapping, darg)
927 927 if isinstance(d, mappable):
928 928 lm = context.overlaymap(mapping, d.tomap(context))
929 929 return runsymbol(context, lm, memb)
930 930 try:
931 931 return d.getmember(context, mapping, memb)
932 932 except error.ParseError as err:
933 933 sym = findsymbolicname(darg)
934 934 if not sym:
935 935 raise
936 936 hint = _("keyword '%s' does not support member operation") % sym
937 937 raise error.ParseError(bytes(err), hint=hint)
938 938
939 939 def runnegate(context, mapping, data):
940 940 data = evalinteger(context, mapping, data,
941 941 _('negation needs an integer argument'))
942 942 return -data
943 943
944 944 def runarithmetic(context, mapping, data):
945 945 func, left, right = data
946 946 left = evalinteger(context, mapping, left,
947 947 _('arithmetic only defined on integers'))
948 948 right = evalinteger(context, mapping, right,
949 949 _('arithmetic only defined on integers'))
950 950 try:
951 951 return func(left, right)
952 952 except ZeroDivisionError:
953 953 raise error.Abort(_('division by zero is not defined'))
954 954
955 955 def joinitems(itemiter, sep):
956 956 """Join items with the separator; Returns generator of bytes"""
957 957 first = True
958 958 for x in itemiter:
959 959 if first:
960 960 first = False
961 961 elif sep:
962 962 yield sep
963 963 yield x
General Comments 0
You need to be logged in to leave comments. Login now