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