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