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