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