##// END OF EJS Templates
templater: drop global exception catcher from runfilter() (API)...
Yuya Nishihara -
r37248:dc4bb142 default
parent child Browse files
Show More
@@ -1,505 +1,503
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 dateutil,
20 20 stringutil,
21 21 )
22 22
23 23 class ResourceUnavailable(error.Abort):
24 24 pass
25 25
26 26 class TemplateNotFound(error.Abort):
27 27 pass
28 28
29 29 # stub for representing a date type; may be a real date type that can
30 30 # provide a readable string value
31 31 class date(object):
32 32 pass
33 33
34 34 class hybrid(object):
35 35 """Wrapper for list or dict to support legacy template
36 36
37 37 This class allows us to handle both:
38 38 - "{files}" (legacy command-line-specific list hack) and
39 39 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
40 40 and to access raw values:
41 41 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
42 42 - "{get(extras, key)}"
43 43 - "{files|json}"
44 44 """
45 45
46 46 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
47 47 if gen is not None:
48 48 self.gen = gen # generator or function returning generator
49 49 self._values = values
50 50 self._makemap = makemap
51 51 self.joinfmt = joinfmt
52 52 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
53 53 def gen(self):
54 54 """Default generator to stringify this as {join(self, ' ')}"""
55 55 for i, x in enumerate(self._values):
56 56 if i > 0:
57 57 yield ' '
58 58 yield self.joinfmt(x)
59 59 def itermaps(self):
60 60 makemap = self._makemap
61 61 for x in self._values:
62 62 yield makemap(x)
63 63 def __contains__(self, x):
64 64 return x in self._values
65 65 def __getitem__(self, key):
66 66 return self._values[key]
67 67 def __len__(self):
68 68 return len(self._values)
69 69 def __iter__(self):
70 70 return iter(self._values)
71 71 def __getattr__(self, name):
72 72 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
73 73 r'itervalues', r'keys', r'values'):
74 74 raise AttributeError(name)
75 75 return getattr(self._values, name)
76 76
77 77 class mappable(object):
78 78 """Wrapper for non-list/dict object to support map operation
79 79
80 80 This class allows us to handle both:
81 81 - "{manifest}"
82 82 - "{manifest % '{rev}:{node}'}"
83 83 - "{manifest.rev}"
84 84
85 85 Unlike a hybrid, this does not simulate the behavior of the underling
86 86 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
87 87 the inner object.
88 88 """
89 89
90 90 def __init__(self, gen, key, value, makemap):
91 91 if gen is not None:
92 92 self.gen = gen # generator or function returning generator
93 93 self._key = key
94 94 self._value = value # may be generator of strings
95 95 self._makemap = makemap
96 96
97 97 def gen(self):
98 98 yield pycompat.bytestr(self._value)
99 99
100 100 def tomap(self):
101 101 return self._makemap(self._key)
102 102
103 103 def itermaps(self):
104 104 yield self.tomap()
105 105
106 106 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
107 107 """Wrap data to support both dict-like and string-like operations"""
108 108 prefmt = pycompat.identity
109 109 if fmt is None:
110 110 fmt = '%s=%s'
111 111 prefmt = pycompat.bytestr
112 112 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
113 113 lambda k: fmt % (prefmt(k), prefmt(data[k])))
114 114
115 115 def hybridlist(data, name, fmt=None, gen=None):
116 116 """Wrap data to support both list-like and string-like operations"""
117 117 prefmt = pycompat.identity
118 118 if fmt is None:
119 119 fmt = '%s'
120 120 prefmt = pycompat.bytestr
121 121 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
122 122
123 123 def unwraphybrid(thing):
124 124 """Return an object which can be stringified possibly by using a legacy
125 125 template"""
126 126 gen = getattr(thing, 'gen', None)
127 127 if gen is None:
128 128 return thing
129 129 if callable(gen):
130 130 return gen()
131 131 return gen
132 132
133 133 def unwrapvalue(thing):
134 134 """Move the inner value object out of the wrapper"""
135 135 if not util.safehasattr(thing, '_value'):
136 136 return thing
137 137 return thing._value
138 138
139 139 def wraphybridvalue(container, key, value):
140 140 """Wrap an element of hybrid container to be mappable
141 141
142 142 The key is passed to the makemap function of the given container, which
143 143 should be an item generated by iter(container).
144 144 """
145 145 makemap = getattr(container, '_makemap', None)
146 146 if makemap is None:
147 147 return value
148 148 if util.safehasattr(value, '_makemap'):
149 149 # a nested hybrid list/dict, which has its own way of map operation
150 150 return value
151 151 return mappable(None, key, value, makemap)
152 152
153 153 def compatdict(context, mapping, name, data, key='key', value='value',
154 154 fmt=None, plural=None, separator=' '):
155 155 """Wrap data like hybriddict(), but also supports old-style list template
156 156
157 157 This exists for backward compatibility with the old-style template. Use
158 158 hybriddict() for new template keywords.
159 159 """
160 160 c = [{key: k, value: v} for k, v in data.iteritems()]
161 161 f = _showcompatlist(context, mapping, name, c, plural, separator)
162 162 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
163 163
164 164 def compatlist(context, mapping, name, data, element=None, fmt=None,
165 165 plural=None, separator=' '):
166 166 """Wrap data like hybridlist(), but also supports old-style list template
167 167
168 168 This exists for backward compatibility with the old-style template. Use
169 169 hybridlist() for new template keywords.
170 170 """
171 171 f = _showcompatlist(context, mapping, name, data, plural, separator)
172 172 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
173 173
174 174 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
175 175 """Return a generator that renders old-style list template
176 176
177 177 name is name of key in template map.
178 178 values is list of strings or dicts.
179 179 plural is plural of name, if not simply name + 's'.
180 180 separator is used to join values as a string
181 181
182 182 expansion works like this, given name 'foo'.
183 183
184 184 if values is empty, expand 'no_foos'.
185 185
186 186 if 'foo' not in template map, return values as a string,
187 187 joined by 'separator'.
188 188
189 189 expand 'start_foos'.
190 190
191 191 for each value, expand 'foo'. if 'last_foo' in template
192 192 map, expand it instead of 'foo' for last key.
193 193
194 194 expand 'end_foos'.
195 195 """
196 196 if not plural:
197 197 plural = name + 's'
198 198 if not values:
199 199 noname = 'no_' + plural
200 200 if context.preload(noname):
201 201 yield context.process(noname, mapping)
202 202 return
203 203 if not context.preload(name):
204 204 if isinstance(values[0], bytes):
205 205 yield separator.join(values)
206 206 else:
207 207 for v in values:
208 208 r = dict(v)
209 209 r.update(mapping)
210 210 yield r
211 211 return
212 212 startname = 'start_' + plural
213 213 if context.preload(startname):
214 214 yield context.process(startname, mapping)
215 215 def one(v, tag=name):
216 216 vmapping = {}
217 217 try:
218 218 vmapping.update(v)
219 219 # Python 2 raises ValueError if the type of v is wrong. Python
220 220 # 3 raises TypeError.
221 221 except (AttributeError, TypeError, ValueError):
222 222 try:
223 223 # Python 2 raises ValueError trying to destructure an e.g.
224 224 # bytes. Python 3 raises TypeError.
225 225 for a, b in v:
226 226 vmapping[a] = b
227 227 except (TypeError, ValueError):
228 228 vmapping[name] = v
229 229 vmapping = context.overlaymap(mapping, vmapping)
230 230 return context.process(tag, vmapping)
231 231 lastname = 'last_' + name
232 232 if context.preload(lastname):
233 233 last = values.pop()
234 234 else:
235 235 last = None
236 236 for v in values:
237 237 yield one(v)
238 238 if last is not None:
239 239 yield one(last, tag=lastname)
240 240 endname = 'end_' + plural
241 241 if context.preload(endname):
242 242 yield context.process(endname, mapping)
243 243
244 244 def flatten(thing):
245 245 """Yield a single stream from a possibly nested set of iterators"""
246 246 thing = unwraphybrid(thing)
247 247 if isinstance(thing, bytes):
248 248 yield thing
249 249 elif isinstance(thing, str):
250 250 # We can only hit this on Python 3, and it's here to guard
251 251 # against infinite recursion.
252 252 raise error.ProgrammingError('Mercurial IO including templates is done'
253 253 ' with bytes, not strings, got %r' % thing)
254 254 elif thing is None:
255 255 pass
256 256 elif not util.safehasattr(thing, '__iter__'):
257 257 yield pycompat.bytestr(thing)
258 258 else:
259 259 for i in thing:
260 260 i = unwraphybrid(i)
261 261 if isinstance(i, bytes):
262 262 yield i
263 263 elif i is None:
264 264 pass
265 265 elif not util.safehasattr(i, '__iter__'):
266 266 yield pycompat.bytestr(i)
267 267 else:
268 268 for j in flatten(i):
269 269 yield j
270 270
271 271 def stringify(thing):
272 272 """Turn values into bytes by converting into text and concatenating them"""
273 273 if isinstance(thing, bytes):
274 274 return thing # retain localstr to be round-tripped
275 275 return b''.join(flatten(thing))
276 276
277 277 def findsymbolicname(arg):
278 278 """Find symbolic name for the given compiled expression; returns None
279 279 if nothing found reliably"""
280 280 while True:
281 281 func, data = arg
282 282 if func is runsymbol:
283 283 return data
284 284 elif func is runfilter:
285 285 arg = data[0]
286 286 else:
287 287 return None
288 288
289 289 def evalrawexp(context, mapping, arg):
290 290 """Evaluate given argument as a bare template object which may require
291 291 further processing (such as folding generator of strings)"""
292 292 func, data = arg
293 293 return func(context, mapping, data)
294 294
295 295 def evalfuncarg(context, mapping, arg):
296 296 """Evaluate given argument as value type"""
297 297 return _unwrapvalue(evalrawexp(context, mapping, arg))
298 298
299 299 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
300 300 # is fixed. we can't do that right now because join() has to take a generator
301 301 # of byte strings as it is, not a lazy byte string.
302 302 def _unwrapvalue(thing):
303 303 thing = unwrapvalue(thing)
304 304 # evalrawexp() may return string, generator of strings or arbitrary object
305 305 # such as date tuple, but filter does not want generator.
306 306 if isinstance(thing, types.GeneratorType):
307 307 thing = stringify(thing)
308 308 return thing
309 309
310 310 def evalboolean(context, mapping, arg):
311 311 """Evaluate given argument as boolean, but also takes boolean literals"""
312 312 func, data = arg
313 313 if func is runsymbol:
314 314 thing = func(context, mapping, data, default=None)
315 315 if thing is None:
316 316 # not a template keyword, takes as a boolean literal
317 317 thing = stringutil.parsebool(data)
318 318 else:
319 319 thing = func(context, mapping, data)
320 320 thing = unwrapvalue(thing)
321 321 if isinstance(thing, bool):
322 322 return thing
323 323 # other objects are evaluated as strings, which means 0 is True, but
324 324 # empty dict/list should be False as they are expected to be ''
325 325 return bool(stringify(thing))
326 326
327 327 def evaldate(context, mapping, arg, err=None):
328 328 """Evaluate given argument as a date tuple or a date string; returns
329 329 a (unixtime, offset) tuple"""
330 330 return unwrapdate(evalrawexp(context, mapping, arg), err)
331 331
332 332 def unwrapdate(thing, err=None):
333 333 thing = _unwrapvalue(thing)
334 334 try:
335 335 return dateutil.parsedate(thing)
336 336 except AttributeError:
337 337 raise error.ParseError(err or _('not a date tuple nor a string'))
338 338 except error.ParseError:
339 339 if not err:
340 340 raise
341 341 raise error.ParseError(err)
342 342
343 343 def evalinteger(context, mapping, arg, err=None):
344 344 return unwrapinteger(evalrawexp(context, mapping, arg), err)
345 345
346 346 def unwrapinteger(thing, err=None):
347 347 thing = _unwrapvalue(thing)
348 348 try:
349 349 return int(thing)
350 350 except (TypeError, ValueError):
351 351 raise error.ParseError(err or _('not an integer'))
352 352
353 353 def evalstring(context, mapping, arg):
354 354 return stringify(evalrawexp(context, mapping, arg))
355 355
356 356 def evalstringliteral(context, mapping, arg):
357 357 """Evaluate given argument as string template, but returns symbol name
358 358 if it is unknown"""
359 359 func, data = arg
360 360 if func is runsymbol:
361 361 thing = func(context, mapping, data, default=data)
362 362 else:
363 363 thing = func(context, mapping, data)
364 364 return stringify(thing)
365 365
366 366 _unwrapfuncbytype = {
367 367 None: _unwrapvalue,
368 368 bytes: stringify,
369 369 date: unwrapdate,
370 370 int: unwrapinteger,
371 371 }
372 372
373 373 def unwrapastype(thing, typ):
374 374 """Move the inner value object out of the wrapper and coerce its type"""
375 375 try:
376 376 f = _unwrapfuncbytype[typ]
377 377 except KeyError:
378 378 raise error.ProgrammingError('invalid type specified: %r' % typ)
379 379 return f(thing)
380 380
381 381 def runinteger(context, mapping, data):
382 382 return int(data)
383 383
384 384 def runstring(context, mapping, data):
385 385 return data
386 386
387 387 def _recursivesymbolblocker(key):
388 388 def showrecursion(**args):
389 389 raise error.Abort(_("recursive reference '%s' in template") % key)
390 390 return showrecursion
391 391
392 392 def runsymbol(context, mapping, key, default=''):
393 393 v = context.symbol(mapping, key)
394 394 if v is None:
395 395 # put poison to cut recursion. we can't move this to parsing phase
396 396 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
397 397 safemapping = mapping.copy()
398 398 safemapping[key] = _recursivesymbolblocker(key)
399 399 try:
400 400 v = context.process(key, safemapping)
401 401 except TemplateNotFound:
402 402 v = default
403 403 if callable(v) and getattr(v, '_requires', None) is None:
404 404 # old templatekw: expand all keywords and resources
405 405 # (TODO: deprecate this after porting web template keywords to new API)
406 406 props = {k: context._resources.lookup(context, mapping, k)
407 407 for k in context._resources.knownkeys()}
408 408 # pass context to _showcompatlist() through templatekw._showlist()
409 409 props['templ'] = context
410 410 props.update(mapping)
411 411 return v(**pycompat.strkwargs(props))
412 412 if callable(v):
413 413 # new templatekw
414 414 try:
415 415 return v(context, mapping)
416 416 except ResourceUnavailable:
417 417 # unsupported keyword is mapped to empty just like unknown keyword
418 418 return None
419 419 return v
420 420
421 421 def runtemplate(context, mapping, template):
422 422 for arg in template:
423 423 yield evalrawexp(context, mapping, arg)
424 424
425 425 def runfilter(context, mapping, data):
426 426 arg, filt = data
427 427 thing = evalrawexp(context, mapping, arg)
428 428 try:
429 429 thing = unwrapastype(thing, getattr(filt, '_intype', None))
430 430 return filt(thing)
431 except (ValueError, AttributeError, TypeError):
432 raise error.Abort(_formatfiltererror(arg, filt))
433 431 except error.ParseError as e:
434 432 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
435 433
436 434 def _formatfiltererror(arg, filt):
437 435 fn = pycompat.sysbytes(filt.__name__)
438 436 sym = findsymbolicname(arg)
439 437 if not sym:
440 438 return _("incompatible use of template filter '%s'") % fn
441 439 return (_("template filter '%s' is not compatible with keyword '%s'")
442 440 % (fn, sym))
443 441
444 442 def runmap(context, mapping, data):
445 443 darg, targ = data
446 444 d = evalrawexp(context, mapping, darg)
447 445 if util.safehasattr(d, 'itermaps'):
448 446 diter = d.itermaps()
449 447 else:
450 448 try:
451 449 diter = iter(d)
452 450 except TypeError:
453 451 sym = findsymbolicname(darg)
454 452 if sym:
455 453 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
456 454 else:
457 455 raise error.ParseError(_("%r is not iterable") % d)
458 456
459 457 for i, v in enumerate(diter):
460 458 if isinstance(v, dict):
461 459 lm = context.overlaymap(mapping, v)
462 460 lm['index'] = i
463 461 yield evalrawexp(context, lm, targ)
464 462 else:
465 463 # v is not an iterable of dicts, this happen when 'key'
466 464 # has been fully expanded already and format is useless.
467 465 # If so, return the expanded value.
468 466 yield v
469 467
470 468 def runmember(context, mapping, data):
471 469 darg, memb = data
472 470 d = evalrawexp(context, mapping, darg)
473 471 if util.safehasattr(d, 'tomap'):
474 472 lm = context.overlaymap(mapping, d.tomap())
475 473 return runsymbol(context, lm, memb)
476 474 if util.safehasattr(d, 'get'):
477 475 return getdictitem(d, memb)
478 476
479 477 sym = findsymbolicname(darg)
480 478 if sym:
481 479 raise error.ParseError(_("keyword '%s' has no member") % sym)
482 480 else:
483 481 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
484 482
485 483 def runnegate(context, mapping, data):
486 484 data = evalinteger(context, mapping, data,
487 485 _('negation needs an integer argument'))
488 486 return -data
489 487
490 488 def runarithmetic(context, mapping, data):
491 489 func, left, right = data
492 490 left = evalinteger(context, mapping, left,
493 491 _('arithmetic only defined on integers'))
494 492 right = evalinteger(context, mapping, right,
495 493 _('arithmetic only defined on integers'))
496 494 try:
497 495 return func(left, right)
498 496 except ZeroDivisionError:
499 497 raise error.Abort(_('division by zero is not defined'))
500 498
501 499 def getdictitem(dictarg, key):
502 500 val = dictarg.get(key)
503 501 if val is None:
504 502 return
505 503 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now