##// END OF EJS Templates
templateutil: move flatten() from templater...
Yuya Nishihara -
r37174:888507ec default
parent child Browse files
Show More
@@ -1,914 +1,887 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 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 30 returns a generator of dicts.)
31 31
32 32 None
33 33 sometimes represents an empty value, which can be stringified to ''.
34 34
35 35 True, False, int, float
36 36 can be stringified as such.
37 37
38 38 date tuple
39 39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
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 mappable
46 46 represents a scalar printable value, also supports % operator.
47 47 """
48 48
49 49 from __future__ import absolute_import, print_function
50 50
51 51 import abc
52 52 import os
53 53
54 54 from .i18n import _
55 55 from . import (
56 56 config,
57 57 encoding,
58 58 error,
59 59 parser,
60 60 pycompat,
61 61 templatefilters,
62 62 templatefuncs,
63 63 templateutil,
64 64 util,
65 65 )
66 66 from .utils import (
67 67 stringutil,
68 68 )
69 69
70 70 # template parsing
71 71
72 72 elements = {
73 73 # token-type: binding-strength, primary, prefix, infix, suffix
74 74 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
75 75 ".": (18, None, None, (".", 18), None),
76 76 "%": (15, None, None, ("%", 15), None),
77 77 "|": (15, None, None, ("|", 15), None),
78 78 "*": (5, None, None, ("*", 5), None),
79 79 "/": (5, None, None, ("/", 5), None),
80 80 "+": (4, None, None, ("+", 4), None),
81 81 "-": (4, None, ("negate", 19), ("-", 4), None),
82 82 "=": (3, None, None, ("keyvalue", 3), None),
83 83 ",": (2, None, None, ("list", 2), None),
84 84 ")": (0, None, None, None, None),
85 85 "integer": (0, "integer", None, None, None),
86 86 "symbol": (0, "symbol", None, None, None),
87 87 "string": (0, "string", None, None, None),
88 88 "template": (0, "template", None, None, None),
89 89 "end": (0, None, None, None, None),
90 90 }
91 91
92 92 def tokenize(program, start, end, term=None):
93 93 """Parse a template expression into a stream of tokens, which must end
94 94 with term if specified"""
95 95 pos = start
96 96 program = pycompat.bytestr(program)
97 97 while pos < end:
98 98 c = program[pos]
99 99 if c.isspace(): # skip inter-token whitespace
100 100 pass
101 101 elif c in "(=,).%|+-*/": # handle simple operators
102 102 yield (c, None, pos)
103 103 elif c in '"\'': # handle quoted templates
104 104 s = pos + 1
105 105 data, pos = _parsetemplate(program, s, end, c)
106 106 yield ('template', data, s)
107 107 pos -= 1
108 108 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
109 109 # handle quoted strings
110 110 c = program[pos + 1]
111 111 s = pos = pos + 2
112 112 while pos < end: # find closing quote
113 113 d = program[pos]
114 114 if d == '\\': # skip over escaped characters
115 115 pos += 2
116 116 continue
117 117 if d == c:
118 118 yield ('string', program[s:pos], s)
119 119 break
120 120 pos += 1
121 121 else:
122 122 raise error.ParseError(_("unterminated string"), s)
123 123 elif c.isdigit():
124 124 s = pos
125 125 while pos < end:
126 126 d = program[pos]
127 127 if not d.isdigit():
128 128 break
129 129 pos += 1
130 130 yield ('integer', program[s:pos], s)
131 131 pos -= 1
132 132 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
133 133 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
134 134 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
135 135 # where some of nested templates were preprocessed as strings and
136 136 # then compiled. therefore, \"...\" was allowed. (issue4733)
137 137 #
138 138 # processing flow of _evalifliteral() at 5ab28a2e9962:
139 139 # outer template string -> stringify() -> compiletemplate()
140 140 # ------------------------ ------------ ------------------
141 141 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
142 142 # ~~~~~~~~
143 143 # escaped quoted string
144 144 if c == 'r':
145 145 pos += 1
146 146 token = 'string'
147 147 else:
148 148 token = 'template'
149 149 quote = program[pos:pos + 2]
150 150 s = pos = pos + 2
151 151 while pos < end: # find closing escaped quote
152 152 if program.startswith('\\\\\\', pos, end):
153 153 pos += 4 # skip over double escaped characters
154 154 continue
155 155 if program.startswith(quote, pos, end):
156 156 # interpret as if it were a part of an outer string
157 157 data = parser.unescapestr(program[s:pos])
158 158 if token == 'template':
159 159 data = _parsetemplate(data, 0, len(data))[0]
160 160 yield (token, data, s)
161 161 pos += 1
162 162 break
163 163 pos += 1
164 164 else:
165 165 raise error.ParseError(_("unterminated string"), s)
166 166 elif c.isalnum() or c in '_':
167 167 s = pos
168 168 pos += 1
169 169 while pos < end: # find end of symbol
170 170 d = program[pos]
171 171 if not (d.isalnum() or d == "_"):
172 172 break
173 173 pos += 1
174 174 sym = program[s:pos]
175 175 yield ('symbol', sym, s)
176 176 pos -= 1
177 177 elif c == term:
178 178 yield ('end', None, pos)
179 179 return
180 180 else:
181 181 raise error.ParseError(_("syntax error"), pos)
182 182 pos += 1
183 183 if term:
184 184 raise error.ParseError(_("unterminated template expansion"), start)
185 185 yield ('end', None, pos)
186 186
187 187 def _parsetemplate(tmpl, start, stop, quote=''):
188 188 r"""
189 189 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
190 190 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
191 191 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
192 192 ([('string', 'foo'), ('symbol', 'bar')], 9)
193 193 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
194 194 ([('string', 'foo')], 4)
195 195 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
196 196 ([('string', 'foo"'), ('string', 'bar')], 9)
197 197 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
198 198 ([('string', 'foo\\')], 6)
199 199 """
200 200 parsed = []
201 201 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
202 202 if typ == 'string':
203 203 parsed.append((typ, val))
204 204 elif typ == 'template':
205 205 parsed.append(val)
206 206 elif typ == 'end':
207 207 return parsed, pos
208 208 else:
209 209 raise error.ProgrammingError('unexpected type: %s' % typ)
210 210 raise error.ProgrammingError('unterminated scanning of template')
211 211
212 212 def scantemplate(tmpl, raw=False):
213 213 r"""Scan (type, start, end) positions of outermost elements in template
214 214
215 215 If raw=True, a backslash is not taken as an escape character just like
216 216 r'' string in Python. Note that this is different from r'' literal in
217 217 template in that no template fragment can appear in r'', e.g. r'{foo}'
218 218 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
219 219 'foo'.
220 220
221 221 >>> list(scantemplate(b'foo{bar}"baz'))
222 222 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
223 223 >>> list(scantemplate(b'outer{"inner"}outer'))
224 224 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
225 225 >>> list(scantemplate(b'foo\\{escaped}'))
226 226 [('string', 0, 5), ('string', 5, 13)]
227 227 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
228 228 [('string', 0, 4), ('template', 4, 13)]
229 229 """
230 230 last = None
231 231 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
232 232 if last:
233 233 yield last + (pos,)
234 234 if typ == 'end':
235 235 return
236 236 else:
237 237 last = (typ, pos)
238 238 raise error.ProgrammingError('unterminated scanning of template')
239 239
240 240 def _scantemplate(tmpl, start, stop, quote='', raw=False):
241 241 """Parse template string into chunks of strings and template expressions"""
242 242 sepchars = '{' + quote
243 243 unescape = [parser.unescapestr, pycompat.identity][raw]
244 244 pos = start
245 245 p = parser.parser(elements)
246 246 try:
247 247 while pos < stop:
248 248 n = min((tmpl.find(c, pos, stop) for c in sepchars),
249 249 key=lambda n: (n < 0, n))
250 250 if n < 0:
251 251 yield ('string', unescape(tmpl[pos:stop]), pos)
252 252 pos = stop
253 253 break
254 254 c = tmpl[n:n + 1]
255 255 bs = 0 # count leading backslashes
256 256 if not raw:
257 257 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
258 258 if bs % 2 == 1:
259 259 # escaped (e.g. '\{', '\\\{', but not '\\{')
260 260 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
261 261 pos = n + 1
262 262 continue
263 263 if n > pos:
264 264 yield ('string', unescape(tmpl[pos:n]), pos)
265 265 if c == quote:
266 266 yield ('end', None, n + 1)
267 267 return
268 268
269 269 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
270 270 if not tmpl.startswith('}', pos):
271 271 raise error.ParseError(_("invalid token"), pos)
272 272 yield ('template', parseres, n)
273 273 pos += 1
274 274
275 275 if quote:
276 276 raise error.ParseError(_("unterminated string"), start)
277 277 except error.ParseError as inst:
278 278 if len(inst.args) > 1: # has location
279 279 loc = inst.args[1]
280 280 # Offset the caret location by the number of newlines before the
281 281 # location of the error, since we will replace one-char newlines
282 282 # with the two-char literal r'\n'.
283 283 offset = tmpl[:loc].count('\n')
284 284 tmpl = tmpl.replace('\n', br'\n')
285 285 # We want the caret to point to the place in the template that
286 286 # failed to parse, but in a hint we get a open paren at the
287 287 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
288 288 # to line up the caret with the location of the error.
289 289 inst.hint = (tmpl + '\n'
290 290 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
291 291 raise
292 292 yield ('end', None, pos)
293 293
294 294 def _unnesttemplatelist(tree):
295 295 """Expand list of templates to node tuple
296 296
297 297 >>> def f(tree):
298 298 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
299 299 >>> f((b'template', []))
300 300 (string '')
301 301 >>> f((b'template', [(b'string', b'foo')]))
302 302 (string 'foo')
303 303 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
304 304 (template
305 305 (string 'foo')
306 306 (symbol 'rev'))
307 307 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
308 308 (template
309 309 (symbol 'rev'))
310 310 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
311 311 (string 'foo')
312 312 """
313 313 if not isinstance(tree, tuple):
314 314 return tree
315 315 op = tree[0]
316 316 if op != 'template':
317 317 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
318 318
319 319 assert len(tree) == 2
320 320 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
321 321 if not xs:
322 322 return ('string', '') # empty template ""
323 323 elif len(xs) == 1 and xs[0][0] == 'string':
324 324 return xs[0] # fast path for string with no template fragment "x"
325 325 else:
326 326 return (op,) + xs
327 327
328 328 def parse(tmpl):
329 329 """Parse template string into tree"""
330 330 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
331 331 assert pos == len(tmpl), 'unquoted template should be consumed'
332 332 return _unnesttemplatelist(('template', parsed))
333 333
334 334 def _parseexpr(expr):
335 335 """Parse a template expression into tree
336 336
337 337 >>> _parseexpr(b'"foo"')
338 338 ('string', 'foo')
339 339 >>> _parseexpr(b'foo(bar)')
340 340 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
341 341 >>> _parseexpr(b'foo(')
342 342 Traceback (most recent call last):
343 343 ...
344 344 ParseError: ('not a prefix: end', 4)
345 345 >>> _parseexpr(b'"foo" "bar"')
346 346 Traceback (most recent call last):
347 347 ...
348 348 ParseError: ('invalid token', 7)
349 349 """
350 350 p = parser.parser(elements)
351 351 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
352 352 if pos != len(expr):
353 353 raise error.ParseError(_('invalid token'), pos)
354 354 return _unnesttemplatelist(tree)
355 355
356 356 def prettyformat(tree):
357 357 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
358 358
359 359 def compileexp(exp, context, curmethods):
360 360 """Compile parsed template tree to (func, data) pair"""
361 361 if not exp:
362 362 raise error.ParseError(_("missing argument"))
363 363 t = exp[0]
364 364 if t in curmethods:
365 365 return curmethods[t](exp, context)
366 366 raise error.ParseError(_("unknown method '%s'") % t)
367 367
368 368 # template evaluation
369 369
370 370 def getsymbol(exp):
371 371 if exp[0] == 'symbol':
372 372 return exp[1]
373 373 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
374 374
375 375 def getlist(x):
376 376 if not x:
377 377 return []
378 378 if x[0] == 'list':
379 379 return getlist(x[1]) + [x[2]]
380 380 return [x]
381 381
382 382 def gettemplate(exp, context):
383 383 """Compile given template tree or load named template from map file;
384 384 returns (func, data) pair"""
385 385 if exp[0] in ('template', 'string'):
386 386 return compileexp(exp, context, methods)
387 387 if exp[0] == 'symbol':
388 388 # unlike runsymbol(), here 'symbol' is always taken as template name
389 389 # even if it exists in mapping. this allows us to override mapping
390 390 # by web templates, e.g. 'changelogtag' is redefined in map file.
391 391 return context._load(exp[1])
392 392 raise error.ParseError(_("expected template specifier"))
393 393
394 394 def _runrecursivesymbol(context, mapping, key):
395 395 raise error.Abort(_("recursive reference '%s' in template") % key)
396 396
397 397 def buildtemplate(exp, context):
398 398 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
399 399 return (templateutil.runtemplate, ctmpl)
400 400
401 401 def buildfilter(exp, context):
402 402 n = getsymbol(exp[2])
403 403 if n in context._filters:
404 404 filt = context._filters[n]
405 405 arg = compileexp(exp[1], context, methods)
406 406 return (templateutil.runfilter, (arg, filt))
407 407 if n in context._funcs:
408 408 f = context._funcs[n]
409 409 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
410 410 return (f, args)
411 411 raise error.ParseError(_("unknown function '%s'") % n)
412 412
413 413 def buildmap(exp, context):
414 414 darg = compileexp(exp[1], context, methods)
415 415 targ = gettemplate(exp[2], context)
416 416 return (templateutil.runmap, (darg, targ))
417 417
418 418 def buildmember(exp, context):
419 419 darg = compileexp(exp[1], context, methods)
420 420 memb = getsymbol(exp[2])
421 421 return (templateutil.runmember, (darg, memb))
422 422
423 423 def buildnegate(exp, context):
424 424 arg = compileexp(exp[1], context, exprmethods)
425 425 return (templateutil.runnegate, arg)
426 426
427 427 def buildarithmetic(exp, context, func):
428 428 left = compileexp(exp[1], context, exprmethods)
429 429 right = compileexp(exp[2], context, exprmethods)
430 430 return (templateutil.runarithmetic, (func, left, right))
431 431
432 432 def buildfunc(exp, context):
433 433 n = getsymbol(exp[1])
434 434 if n in context._funcs:
435 435 f = context._funcs[n]
436 436 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
437 437 return (f, args)
438 438 if n in context._filters:
439 439 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
440 440 if len(args) != 1:
441 441 raise error.ParseError(_("filter %s expects one argument") % n)
442 442 f = context._filters[n]
443 443 return (templateutil.runfilter, (args[0], f))
444 444 raise error.ParseError(_("unknown function '%s'") % n)
445 445
446 446 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
447 447 """Compile parsed tree of function arguments into list or dict of
448 448 (func, data) pairs
449 449
450 450 >>> context = engine(lambda t: (templateutil.runsymbol, t))
451 451 >>> def fargs(expr, argspec):
452 452 ... x = _parseexpr(expr)
453 453 ... n = getsymbol(x[1])
454 454 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
455 455 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
456 456 ['l', 'k']
457 457 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
458 458 >>> list(args.keys()), list(args[b'opts'].keys())
459 459 (['opts'], ['opts', 'k'])
460 460 """
461 461 def compiledict(xs):
462 462 return util.sortdict((k, compileexp(x, context, curmethods))
463 463 for k, x in xs.iteritems())
464 464 def compilelist(xs):
465 465 return [compileexp(x, context, curmethods) for x in xs]
466 466
467 467 if not argspec:
468 468 # filter or function with no argspec: return list of positional args
469 469 return compilelist(getlist(exp))
470 470
471 471 # function with argspec: return dict of named args
472 472 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
473 473 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
474 474 keyvaluenode='keyvalue', keynode='symbol')
475 475 compargs = util.sortdict()
476 476 if varkey:
477 477 compargs[varkey] = compilelist(treeargs.pop(varkey))
478 478 if optkey:
479 479 compargs[optkey] = compiledict(treeargs.pop(optkey))
480 480 compargs.update(compiledict(treeargs))
481 481 return compargs
482 482
483 483 def buildkeyvaluepair(exp, content):
484 484 raise error.ParseError(_("can't use a key-value pair in this context"))
485 485
486 486 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
487 487 exprmethods = {
488 488 "integer": lambda e, c: (templateutil.runinteger, e[1]),
489 489 "string": lambda e, c: (templateutil.runstring, e[1]),
490 490 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
491 491 "template": buildtemplate,
492 492 "group": lambda e, c: compileexp(e[1], c, exprmethods),
493 493 ".": buildmember,
494 494 "|": buildfilter,
495 495 "%": buildmap,
496 496 "func": buildfunc,
497 497 "keyvalue": buildkeyvaluepair,
498 498 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
499 499 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
500 500 "negate": buildnegate,
501 501 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
502 502 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
503 503 }
504 504
505 505 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
506 506 methods = exprmethods.copy()
507 507 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
508 508
509 509 class _aliasrules(parser.basealiasrules):
510 510 """Parsing and expansion rule set of template aliases"""
511 511 _section = _('template alias')
512 512 _parse = staticmethod(_parseexpr)
513 513
514 514 @staticmethod
515 515 def _trygetfunc(tree):
516 516 """Return (name, args) if tree is func(...) or ...|filter; otherwise
517 517 None"""
518 518 if tree[0] == 'func' and tree[1][0] == 'symbol':
519 519 return tree[1][1], getlist(tree[2])
520 520 if tree[0] == '|' and tree[2][0] == 'symbol':
521 521 return tree[2][1], [tree[1]]
522 522
523 523 def expandaliases(tree, aliases):
524 524 """Return new tree of aliases are expanded"""
525 525 aliasmap = _aliasrules.buildmap(aliases)
526 526 return _aliasrules.expand(aliasmap, tree)
527 527
528 528 # template engine
529 529
530 def _flatten(thing):
531 '''yield a single stream from a possibly nested set of iterators'''
532 thing = templateutil.unwraphybrid(thing)
533 if isinstance(thing, bytes):
534 yield thing
535 elif isinstance(thing, str):
536 # We can only hit this on Python 3, and it's here to guard
537 # against infinite recursion.
538 raise error.ProgrammingError('Mercurial IO including templates is done'
539 ' with bytes, not strings, got %r' % thing)
540 elif thing is None:
541 pass
542 elif not util.safehasattr(thing, '__iter__'):
543 yield pycompat.bytestr(thing)
544 else:
545 for i in thing:
546 i = templateutil.unwraphybrid(i)
547 if isinstance(i, bytes):
548 yield i
549 elif i is None:
550 pass
551 elif not util.safehasattr(i, '__iter__'):
552 yield pycompat.bytestr(i)
553 else:
554 for j in _flatten(i):
555 yield j
556
557 530 def unquotestring(s):
558 531 '''unwrap quotes if any; otherwise returns unmodified string'''
559 532 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
560 533 return s
561 534 return s[1:-1]
562 535
563 536 class resourcemapper(object):
564 537 """Mapper of internal template resources"""
565 538
566 539 __metaclass__ = abc.ABCMeta
567 540
568 541 @abc.abstractmethod
569 542 def availablekeys(self, context, mapping):
570 543 """Return a set of available resource keys based on the given mapping"""
571 544
572 545 @abc.abstractmethod
573 546 def knownkeys(self):
574 547 """Return a set of supported resource keys"""
575 548
576 549 @abc.abstractmethod
577 550 def lookup(self, context, mapping, key):
578 551 """Return a resource for the key if available; otherwise None"""
579 552
580 553 @abc.abstractmethod
581 554 def populatemap(self, context, origmapping, newmapping):
582 555 """Return a dict of additional mapping items which should be paired
583 556 with the given new mapping"""
584 557
585 558 class nullresourcemapper(resourcemapper):
586 559 def availablekeys(self, context, mapping):
587 560 return set()
588 561
589 562 def knownkeys(self):
590 563 return set()
591 564
592 565 def lookup(self, context, mapping, key):
593 566 return None
594 567
595 568 def populatemap(self, context, origmapping, newmapping):
596 569 return {}
597 570
598 571 class engine(object):
599 572 '''template expansion engine.
600 573
601 574 template expansion works like this. a map file contains key=value
602 575 pairs. if value is quoted, it is treated as string. otherwise, it
603 576 is treated as name of template file.
604 577
605 578 templater is asked to expand a key in map. it looks up key, and
606 579 looks for strings like this: {foo}. it expands {foo} by looking up
607 580 foo in map, and substituting it. expansion is recursive: it stops
608 581 when there is no more {foo} to replace.
609 582
610 583 expansion also allows formatting and filtering.
611 584
612 585 format uses key to expand each item in list. syntax is
613 586 {key%format}.
614 587
615 588 filter uses function to transform value. syntax is
616 589 {key|filter1|filter2|...}.'''
617 590
618 591 def __init__(self, loader, filters=None, defaults=None, resources=None,
619 592 aliases=()):
620 593 self._loader = loader
621 594 if filters is None:
622 595 filters = {}
623 596 self._filters = filters
624 597 self._funcs = templatefuncs.funcs # make this a parameter if needed
625 598 if defaults is None:
626 599 defaults = {}
627 600 if resources is None:
628 601 resources = nullresourcemapper()
629 602 self._defaults = defaults
630 603 self._resources = resources
631 604 self._aliasmap = _aliasrules.buildmap(aliases)
632 605 self._cache = {} # key: (func, data)
633 606
634 607 def overlaymap(self, origmapping, newmapping):
635 608 """Create combined mapping from the original mapping and partial
636 609 mapping to override the original"""
637 610 # do not copy symbols which overrides the defaults depending on
638 611 # new resources, so the defaults will be re-evaluated (issue5612)
639 612 knownres = self._resources.knownkeys()
640 613 newres = self._resources.availablekeys(self, newmapping)
641 614 mapping = {k: v for k, v in origmapping.iteritems()
642 615 if (k in knownres # not a symbol per self.symbol()
643 616 or newres.isdisjoint(self._defaultrequires(k)))}
644 617 mapping.update(newmapping)
645 618 mapping.update(
646 619 self._resources.populatemap(self, origmapping, newmapping))
647 620 return mapping
648 621
649 622 def _defaultrequires(self, key):
650 623 """Resource keys required by the specified default symbol function"""
651 624 v = self._defaults.get(key)
652 625 if v is None or not callable(v):
653 626 return ()
654 627 return getattr(v, '_requires', ())
655 628
656 629 def symbol(self, mapping, key):
657 630 """Resolve symbol to value or function; None if nothing found"""
658 631 v = None
659 632 if key not in self._resources.knownkeys():
660 633 v = mapping.get(key)
661 634 if v is None:
662 635 v = self._defaults.get(key)
663 636 return v
664 637
665 638 def resource(self, mapping, key):
666 639 """Return internal data (e.g. cache) used for keyword/function
667 640 evaluation"""
668 641 v = self._resources.lookup(self, mapping, key)
669 642 if v is None:
670 643 raise templateutil.ResourceUnavailable(
671 644 _('template resource not available: %s') % key)
672 645 return v
673 646
674 647 def _load(self, t):
675 648 '''load, parse, and cache a template'''
676 649 if t not in self._cache:
677 650 # put poison to cut recursion while compiling 't'
678 651 self._cache[t] = (_runrecursivesymbol, t)
679 652 try:
680 653 x = parse(self._loader(t))
681 654 if self._aliasmap:
682 655 x = _aliasrules.expand(self._aliasmap, x)
683 656 self._cache[t] = compileexp(x, self, methods)
684 657 except: # re-raises
685 658 del self._cache[t]
686 659 raise
687 660 return self._cache[t]
688 661
689 662 def preload(self, t):
690 663 """Load, parse, and cache the specified template if available"""
691 664 try:
692 665 self._load(t)
693 666 return True
694 667 except templateutil.TemplateNotFound:
695 668 return False
696 669
697 670 def process(self, t, mapping):
698 671 '''Perform expansion. t is name of map element to expand.
699 672 mapping contains added elements for use during expansion. Is a
700 673 generator.'''
701 674 func, data = self._load(t)
702 675 # populate additional items only if they don't exist in the given
703 676 # mapping. this is slightly different from overlaymap() because the
704 677 # initial 'revcache' may contain pre-computed items.
705 678 extramapping = self._resources.populatemap(self, {}, mapping)
706 679 if extramapping:
707 680 extramapping.update(mapping)
708 681 mapping = extramapping
709 return _flatten(func(self, mapping, data))
682 return templateutil.flatten(func(self, mapping, data))
710 683
711 684 engines = {'default': engine}
712 685
713 686 def stylelist():
714 687 paths = templatepaths()
715 688 if not paths:
716 689 return _('no templates found, try `hg debuginstall` for more info')
717 690 dirlist = os.listdir(paths[0])
718 691 stylelist = []
719 692 for file in dirlist:
720 693 split = file.split(".")
721 694 if split[-1] in ('orig', 'rej'):
722 695 continue
723 696 if split[0] == "map-cmdline":
724 697 stylelist.append(split[1])
725 698 return ", ".join(sorted(stylelist))
726 699
727 700 def _readmapfile(mapfile):
728 701 """Load template elements from the given map file"""
729 702 if not os.path.exists(mapfile):
730 703 raise error.Abort(_("style '%s' not found") % mapfile,
731 704 hint=_("available styles: %s") % stylelist())
732 705
733 706 base = os.path.dirname(mapfile)
734 707 conf = config.config(includepaths=templatepaths())
735 708 conf.read(mapfile, remap={'': 'templates'})
736 709
737 710 cache = {}
738 711 tmap = {}
739 712 aliases = []
740 713
741 714 val = conf.get('templates', '__base__')
742 715 if val and val[0] not in "'\"":
743 716 # treat as a pointer to a base class for this style
744 717 path = util.normpath(os.path.join(base, val))
745 718
746 719 # fallback check in template paths
747 720 if not os.path.exists(path):
748 721 for p in templatepaths():
749 722 p2 = util.normpath(os.path.join(p, val))
750 723 if os.path.isfile(p2):
751 724 path = p2
752 725 break
753 726 p3 = util.normpath(os.path.join(p2, "map"))
754 727 if os.path.isfile(p3):
755 728 path = p3
756 729 break
757 730
758 731 cache, tmap, aliases = _readmapfile(path)
759 732
760 733 for key, val in conf['templates'].items():
761 734 if not val:
762 735 raise error.ParseError(_('missing value'),
763 736 conf.source('templates', key))
764 737 if val[0] in "'\"":
765 738 if val[0] != val[-1]:
766 739 raise error.ParseError(_('unmatched quotes'),
767 740 conf.source('templates', key))
768 741 cache[key] = unquotestring(val)
769 742 elif key != '__base__':
770 743 val = 'default', val
771 744 if ':' in val[1]:
772 745 val = val[1].split(':', 1)
773 746 tmap[key] = val[0], os.path.join(base, val[1])
774 747 aliases.extend(conf['templatealias'].items())
775 748 return cache, tmap, aliases
776 749
777 750 class templater(object):
778 751
779 752 def __init__(self, filters=None, defaults=None, resources=None,
780 753 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
781 754 """Create template engine optionally with preloaded template fragments
782 755
783 756 - ``filters``: a dict of functions to transform a value into another.
784 757 - ``defaults``: a dict of symbol values/functions; may be overridden
785 758 by a ``mapping`` dict.
786 759 - ``resources``: a resourcemapper object to look up internal data
787 760 (e.g. cache), inaccessible from user template.
788 761 - ``cache``: a dict of preloaded template fragments.
789 762 - ``aliases``: a list of alias (name, replacement) pairs.
790 763
791 764 self.cache may be updated later to register additional template
792 765 fragments.
793 766 """
794 767 if filters is None:
795 768 filters = {}
796 769 if defaults is None:
797 770 defaults = {}
798 771 if cache is None:
799 772 cache = {}
800 773 self.cache = cache.copy()
801 774 self.map = {}
802 775 self.filters = templatefilters.filters.copy()
803 776 self.filters.update(filters)
804 777 self.defaults = defaults
805 778 self._resources = resources
806 779 self._aliases = aliases
807 780 self.minchunk, self.maxchunk = minchunk, maxchunk
808 781 self.ecache = {}
809 782
810 783 @classmethod
811 784 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
812 785 cache=None, minchunk=1024, maxchunk=65536):
813 786 """Create templater from the specified map file"""
814 787 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
815 788 cache, tmap, aliases = _readmapfile(mapfile)
816 789 t.cache.update(cache)
817 790 t.map = tmap
818 791 t._aliases = aliases
819 792 return t
820 793
821 794 def __contains__(self, key):
822 795 return key in self.cache or key in self.map
823 796
824 797 def load(self, t):
825 798 '''Get the template for the given template name. Use a local cache.'''
826 799 if t not in self.cache:
827 800 try:
828 801 self.cache[t] = util.readfile(self.map[t][1])
829 802 except KeyError as inst:
830 803 raise templateutil.TemplateNotFound(
831 804 _('"%s" not in template map') % inst.args[0])
832 805 except IOError as inst:
833 806 reason = (_('template file %s: %s')
834 807 % (self.map[t][1],
835 808 stringutil.forcebytestr(inst.args[1])))
836 809 raise IOError(inst.args[0], encoding.strfromlocal(reason))
837 810 return self.cache[t]
838 811
839 812 def renderdefault(self, mapping):
840 813 """Render the default unnamed template and return result as string"""
841 814 return self.render('', mapping)
842 815
843 816 def render(self, t, mapping):
844 817 """Render the specified named template and return result as string"""
845 818 return templateutil.stringify(self.generate(t, mapping))
846 819
847 820 def generate(self, t, mapping):
848 821 """Return a generator that renders the specified named template and
849 822 yields chunks"""
850 823 ttype = t in self.map and self.map[t][0] or 'default'
851 824 if ttype not in self.ecache:
852 825 try:
853 826 ecls = engines[ttype]
854 827 except KeyError:
855 828 raise error.Abort(_('invalid template engine: %s') % ttype)
856 829 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
857 830 self._resources, self._aliases)
858 831 proc = self.ecache[ttype]
859 832
860 833 stream = proc.process(t, mapping)
861 834 if self.minchunk:
862 835 stream = util.increasingchunks(stream, min=self.minchunk,
863 836 max=self.maxchunk)
864 837 return stream
865 838
866 839 def templatepaths():
867 840 '''return locations used for template files.'''
868 841 pathsrel = ['templates']
869 842 paths = [os.path.normpath(os.path.join(util.datapath, f))
870 843 for f in pathsrel]
871 844 return [p for p in paths if os.path.isdir(p)]
872 845
873 846 def templatepath(name):
874 847 '''return location of template file. returns None if not found.'''
875 848 for p in templatepaths():
876 849 f = os.path.join(p, name)
877 850 if os.path.exists(f):
878 851 return f
879 852 return None
880 853
881 854 def stylemap(styles, paths=None):
882 855 """Return path to mapfile for a given style.
883 856
884 857 Searches mapfile in the following locations:
885 858 1. templatepath/style/map
886 859 2. templatepath/map-style
887 860 3. templatepath/map
888 861 """
889 862
890 863 if paths is None:
891 864 paths = templatepaths()
892 865 elif isinstance(paths, bytes):
893 866 paths = [paths]
894 867
895 868 if isinstance(styles, bytes):
896 869 styles = [styles]
897 870
898 871 for style in styles:
899 872 # only plain name is allowed to honor template paths
900 873 if (not style
901 874 or style in (pycompat.oscurdir, pycompat.ospardir)
902 875 or pycompat.ossep in style
903 876 or pycompat.osaltsep and pycompat.osaltsep in style):
904 877 continue
905 878 locations = [os.path.join(style, 'map'), 'map-' + style]
906 879 locations.append('map')
907 880
908 881 for path in paths:
909 882 for location in locations:
910 883 mapfile = os.path.join(path, location)
911 884 if os.path.isfile(mapfile):
912 885 return style, mapfile
913 886
914 887 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,451 +1,478 b''
1 1 # templateutil.py - utility for template evaluation
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import types
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 )
18 18 from .utils import (
19 19 stringutil,
20 20 )
21 21
22 22 class ResourceUnavailable(error.Abort):
23 23 pass
24 24
25 25 class TemplateNotFound(error.Abort):
26 26 pass
27 27
28 28 class hybrid(object):
29 29 """Wrapper for list or dict to support legacy template
30 30
31 31 This class allows us to handle both:
32 32 - "{files}" (legacy command-line-specific list hack) and
33 33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 34 and to access raw values:
35 35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 36 - "{get(extras, key)}"
37 37 - "{files|json}"
38 38 """
39 39
40 40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 41 if gen is not None:
42 42 self.gen = gen # generator or function returning generator
43 43 self._values = values
44 44 self._makemap = makemap
45 45 self.joinfmt = joinfmt
46 46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 47 def gen(self):
48 48 """Default generator to stringify this as {join(self, ' ')}"""
49 49 for i, x in enumerate(self._values):
50 50 if i > 0:
51 51 yield ' '
52 52 yield self.joinfmt(x)
53 53 def itermaps(self):
54 54 makemap = self._makemap
55 55 for x in self._values:
56 56 yield makemap(x)
57 57 def __contains__(self, x):
58 58 return x in self._values
59 59 def __getitem__(self, key):
60 60 return self._values[key]
61 61 def __len__(self):
62 62 return len(self._values)
63 63 def __iter__(self):
64 64 return iter(self._values)
65 65 def __getattr__(self, name):
66 66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 67 r'itervalues', r'keys', r'values'):
68 68 raise AttributeError(name)
69 69 return getattr(self._values, name)
70 70
71 71 class mappable(object):
72 72 """Wrapper for non-list/dict object to support map operation
73 73
74 74 This class allows us to handle both:
75 75 - "{manifest}"
76 76 - "{manifest % '{rev}:{node}'}"
77 77 - "{manifest.rev}"
78 78
79 79 Unlike a hybrid, this does not simulate the behavior of the underling
80 80 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 81 """
82 82
83 83 def __init__(self, gen, key, value, makemap):
84 84 if gen is not None:
85 85 self.gen = gen # generator or function returning generator
86 86 self._key = key
87 87 self._value = value # may be generator of strings
88 88 self._makemap = makemap
89 89
90 90 def gen(self):
91 91 yield pycompat.bytestr(self._value)
92 92
93 93 def tomap(self):
94 94 return self._makemap(self._key)
95 95
96 96 def itermaps(self):
97 97 yield self.tomap()
98 98
99 99 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
100 100 """Wrap data to support both dict-like and string-like operations"""
101 101 prefmt = pycompat.identity
102 102 if fmt is None:
103 103 fmt = '%s=%s'
104 104 prefmt = pycompat.bytestr
105 105 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
106 106 lambda k: fmt % (prefmt(k), prefmt(data[k])))
107 107
108 108 def hybridlist(data, name, fmt=None, gen=None):
109 109 """Wrap data to support both list-like and string-like operations"""
110 110 prefmt = pycompat.identity
111 111 if fmt is None:
112 112 fmt = '%s'
113 113 prefmt = pycompat.bytestr
114 114 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
115 115
116 116 def unwraphybrid(thing):
117 117 """Return an object which can be stringified possibly by using a legacy
118 118 template"""
119 119 gen = getattr(thing, 'gen', None)
120 120 if gen is None:
121 121 return thing
122 122 if callable(gen):
123 123 return gen()
124 124 return gen
125 125
126 126 def unwrapvalue(thing):
127 127 """Move the inner value object out of the wrapper"""
128 128 if not util.safehasattr(thing, '_value'):
129 129 return thing
130 130 return thing._value
131 131
132 132 def wraphybridvalue(container, key, value):
133 133 """Wrap an element of hybrid container to be mappable
134 134
135 135 The key is passed to the makemap function of the given container, which
136 136 should be an item generated by iter(container).
137 137 """
138 138 makemap = getattr(container, '_makemap', None)
139 139 if makemap is None:
140 140 return value
141 141 if util.safehasattr(value, '_makemap'):
142 142 # a nested hybrid list/dict, which has its own way of map operation
143 143 return value
144 144 return mappable(None, key, value, makemap)
145 145
146 146 def compatdict(context, mapping, name, data, key='key', value='value',
147 147 fmt=None, plural=None, separator=' '):
148 148 """Wrap data like hybriddict(), but also supports old-style list template
149 149
150 150 This exists for backward compatibility with the old-style template. Use
151 151 hybriddict() for new template keywords.
152 152 """
153 153 c = [{key: k, value: v} for k, v in data.iteritems()]
154 154 f = _showcompatlist(context, mapping, name, c, plural, separator)
155 155 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
156 156
157 157 def compatlist(context, mapping, name, data, element=None, fmt=None,
158 158 plural=None, separator=' '):
159 159 """Wrap data like hybridlist(), but also supports old-style list template
160 160
161 161 This exists for backward compatibility with the old-style template. Use
162 162 hybridlist() for new template keywords.
163 163 """
164 164 f = _showcompatlist(context, mapping, name, data, plural, separator)
165 165 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
166 166
167 167 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
168 168 """Return a generator that renders old-style list template
169 169
170 170 name is name of key in template map.
171 171 values is list of strings or dicts.
172 172 plural is plural of name, if not simply name + 's'.
173 173 separator is used to join values as a string
174 174
175 175 expansion works like this, given name 'foo'.
176 176
177 177 if values is empty, expand 'no_foos'.
178 178
179 179 if 'foo' not in template map, return values as a string,
180 180 joined by 'separator'.
181 181
182 182 expand 'start_foos'.
183 183
184 184 for each value, expand 'foo'. if 'last_foo' in template
185 185 map, expand it instead of 'foo' for last key.
186 186
187 187 expand 'end_foos'.
188 188 """
189 189 if not plural:
190 190 plural = name + 's'
191 191 if not values:
192 192 noname = 'no_' + plural
193 193 if context.preload(noname):
194 194 yield context.process(noname, mapping)
195 195 return
196 196 if not context.preload(name):
197 197 if isinstance(values[0], bytes):
198 198 yield separator.join(values)
199 199 else:
200 200 for v in values:
201 201 r = dict(v)
202 202 r.update(mapping)
203 203 yield r
204 204 return
205 205 startname = 'start_' + plural
206 206 if context.preload(startname):
207 207 yield context.process(startname, mapping)
208 208 def one(v, tag=name):
209 209 vmapping = {}
210 210 try:
211 211 vmapping.update(v)
212 212 # Python 2 raises ValueError if the type of v is wrong. Python
213 213 # 3 raises TypeError.
214 214 except (AttributeError, TypeError, ValueError):
215 215 try:
216 216 # Python 2 raises ValueError trying to destructure an e.g.
217 217 # bytes. Python 3 raises TypeError.
218 218 for a, b in v:
219 219 vmapping[a] = b
220 220 except (TypeError, ValueError):
221 221 vmapping[name] = v
222 222 vmapping = context.overlaymap(mapping, vmapping)
223 223 return context.process(tag, vmapping)
224 224 lastname = 'last_' + name
225 225 if context.preload(lastname):
226 226 last = values.pop()
227 227 else:
228 228 last = None
229 229 for v in values:
230 230 yield one(v)
231 231 if last is not None:
232 232 yield one(last, tag=lastname)
233 233 endname = 'end_' + plural
234 234 if context.preload(endname):
235 235 yield context.process(endname, mapping)
236 236
237 def flatten(thing):
238 """Yield a single stream from a possibly nested set of iterators"""
239 thing = unwraphybrid(thing)
240 if isinstance(thing, bytes):
241 yield thing
242 elif isinstance(thing, str):
243 # We can only hit this on Python 3, and it's here to guard
244 # against infinite recursion.
245 raise error.ProgrammingError('Mercurial IO including templates is done'
246 ' with bytes, not strings, got %r' % thing)
247 elif thing is None:
248 pass
249 elif not util.safehasattr(thing, '__iter__'):
250 yield pycompat.bytestr(thing)
251 else:
252 for i in thing:
253 i = unwraphybrid(i)
254 if isinstance(i, bytes):
255 yield i
256 elif i is None:
257 pass
258 elif not util.safehasattr(i, '__iter__'):
259 yield pycompat.bytestr(i)
260 else:
261 for j in flatten(i):
262 yield j
263
237 264 def stringify(thing):
238 265 """Turn values into bytes by converting into text and concatenating them"""
239 266 thing = unwraphybrid(thing)
240 267 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
241 268 if isinstance(thing, str):
242 269 # This is only reachable on Python 3 (otherwise
243 270 # isinstance(thing, bytes) would have been true), and is
244 271 # here to prevent infinite recursion bugs on Python 3.
245 272 raise error.ProgrammingError(
246 273 'stringify got unexpected unicode string: %r' % thing)
247 274 return "".join([stringify(t) for t in thing if t is not None])
248 275 if thing is None:
249 276 return ""
250 277 return pycompat.bytestr(thing)
251 278
252 279 def findsymbolicname(arg):
253 280 """Find symbolic name for the given compiled expression; returns None
254 281 if nothing found reliably"""
255 282 while True:
256 283 func, data = arg
257 284 if func is runsymbol:
258 285 return data
259 286 elif func is runfilter:
260 287 arg = data[0]
261 288 else:
262 289 return None
263 290
264 291 def evalrawexp(context, mapping, arg):
265 292 """Evaluate given argument as a bare template object which may require
266 293 further processing (such as folding generator of strings)"""
267 294 func, data = arg
268 295 return func(context, mapping, data)
269 296
270 297 def evalfuncarg(context, mapping, arg):
271 298 """Evaluate given argument as value type"""
272 299 thing = evalrawexp(context, mapping, arg)
273 300 thing = unwrapvalue(thing)
274 301 # evalrawexp() may return string, generator of strings or arbitrary object
275 302 # such as date tuple, but filter does not want generator.
276 303 if isinstance(thing, types.GeneratorType):
277 304 thing = stringify(thing)
278 305 return thing
279 306
280 307 def evalboolean(context, mapping, arg):
281 308 """Evaluate given argument as boolean, but also takes boolean literals"""
282 309 func, data = arg
283 310 if func is runsymbol:
284 311 thing = func(context, mapping, data, default=None)
285 312 if thing is None:
286 313 # not a template keyword, takes as a boolean literal
287 314 thing = stringutil.parsebool(data)
288 315 else:
289 316 thing = func(context, mapping, data)
290 317 thing = unwrapvalue(thing)
291 318 if isinstance(thing, bool):
292 319 return thing
293 320 # other objects are evaluated as strings, which means 0 is True, but
294 321 # empty dict/list should be False as they are expected to be ''
295 322 return bool(stringify(thing))
296 323
297 324 def evalinteger(context, mapping, arg, err=None):
298 325 v = evalfuncarg(context, mapping, arg)
299 326 try:
300 327 return int(v)
301 328 except (TypeError, ValueError):
302 329 raise error.ParseError(err or _('not an integer'))
303 330
304 331 def evalstring(context, mapping, arg):
305 332 return stringify(evalrawexp(context, mapping, arg))
306 333
307 334 def evalstringliteral(context, mapping, arg):
308 335 """Evaluate given argument as string template, but returns symbol name
309 336 if it is unknown"""
310 337 func, data = arg
311 338 if func is runsymbol:
312 339 thing = func(context, mapping, data, default=data)
313 340 else:
314 341 thing = func(context, mapping, data)
315 342 return stringify(thing)
316 343
317 344 _evalfuncbytype = {
318 345 bool: evalboolean,
319 346 bytes: evalstring,
320 347 int: evalinteger,
321 348 }
322 349
323 350 def evalastype(context, mapping, arg, typ):
324 351 """Evaluate given argument and coerce its type"""
325 352 try:
326 353 f = _evalfuncbytype[typ]
327 354 except KeyError:
328 355 raise error.ProgrammingError('invalid type specified: %r' % typ)
329 356 return f(context, mapping, arg)
330 357
331 358 def runinteger(context, mapping, data):
332 359 return int(data)
333 360
334 361 def runstring(context, mapping, data):
335 362 return data
336 363
337 364 def _recursivesymbolblocker(key):
338 365 def showrecursion(**args):
339 366 raise error.Abort(_("recursive reference '%s' in template") % key)
340 367 return showrecursion
341 368
342 369 def runsymbol(context, mapping, key, default=''):
343 370 v = context.symbol(mapping, key)
344 371 if v is None:
345 372 # put poison to cut recursion. we can't move this to parsing phase
346 373 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
347 374 safemapping = mapping.copy()
348 375 safemapping[key] = _recursivesymbolblocker(key)
349 376 try:
350 377 v = context.process(key, safemapping)
351 378 except TemplateNotFound:
352 379 v = default
353 380 if callable(v) and getattr(v, '_requires', None) is None:
354 381 # old templatekw: expand all keywords and resources
355 382 # (TODO: deprecate this after porting web template keywords to new API)
356 383 props = {k: context._resources.lookup(context, mapping, k)
357 384 for k in context._resources.knownkeys()}
358 385 # pass context to _showcompatlist() through templatekw._showlist()
359 386 props['templ'] = context
360 387 props.update(mapping)
361 388 return v(**pycompat.strkwargs(props))
362 389 if callable(v):
363 390 # new templatekw
364 391 try:
365 392 return v(context, mapping)
366 393 except ResourceUnavailable:
367 394 # unsupported keyword is mapped to empty just like unknown keyword
368 395 return None
369 396 return v
370 397
371 398 def runtemplate(context, mapping, template):
372 399 for arg in template:
373 400 yield evalrawexp(context, mapping, arg)
374 401
375 402 def runfilter(context, mapping, data):
376 403 arg, filt = data
377 404 thing = evalfuncarg(context, mapping, arg)
378 405 try:
379 406 return filt(thing)
380 407 except (ValueError, AttributeError, TypeError):
381 408 sym = findsymbolicname(arg)
382 409 if sym:
383 410 msg = (_("template filter '%s' is not compatible with keyword '%s'")
384 411 % (pycompat.sysbytes(filt.__name__), sym))
385 412 else:
386 413 msg = (_("incompatible use of template filter '%s'")
387 414 % pycompat.sysbytes(filt.__name__))
388 415 raise error.Abort(msg)
389 416
390 417 def runmap(context, mapping, data):
391 418 darg, targ = data
392 419 d = evalrawexp(context, mapping, darg)
393 420 if util.safehasattr(d, 'itermaps'):
394 421 diter = d.itermaps()
395 422 else:
396 423 try:
397 424 diter = iter(d)
398 425 except TypeError:
399 426 sym = findsymbolicname(darg)
400 427 if sym:
401 428 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
402 429 else:
403 430 raise error.ParseError(_("%r is not iterable") % d)
404 431
405 432 for i, v in enumerate(diter):
406 433 if isinstance(v, dict):
407 434 lm = context.overlaymap(mapping, v)
408 435 lm['index'] = i
409 436 yield evalrawexp(context, lm, targ)
410 437 else:
411 438 # v is not an iterable of dicts, this happen when 'key'
412 439 # has been fully expanded already and format is useless.
413 440 # If so, return the expanded value.
414 441 yield v
415 442
416 443 def runmember(context, mapping, data):
417 444 darg, memb = data
418 445 d = evalrawexp(context, mapping, darg)
419 446 if util.safehasattr(d, 'tomap'):
420 447 lm = context.overlaymap(mapping, d.tomap())
421 448 return runsymbol(context, lm, memb)
422 449 if util.safehasattr(d, 'get'):
423 450 return getdictitem(d, memb)
424 451
425 452 sym = findsymbolicname(darg)
426 453 if sym:
427 454 raise error.ParseError(_("keyword '%s' has no member") % sym)
428 455 else:
429 456 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
430 457
431 458 def runnegate(context, mapping, data):
432 459 data = evalinteger(context, mapping, data,
433 460 _('negation needs an integer argument'))
434 461 return -data
435 462
436 463 def runarithmetic(context, mapping, data):
437 464 func, left, right = data
438 465 left = evalinteger(context, mapping, left,
439 466 _('arithmetic only defined on integers'))
440 467 right = evalinteger(context, mapping, right,
441 468 _('arithmetic only defined on integers'))
442 469 try:
443 470 return func(left, right)
444 471 except ZeroDivisionError:
445 472 raise error.Abort(_('division by zero is not defined'))
446 473
447 474 def getdictitem(dictarg, key):
448 475 val = dictarg.get(key)
449 476 if val is None:
450 477 return
451 478 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now