##// END OF EJS Templates
templater: fix string representation of wrapped None...
Yuya Nishihara -
r38291:354fad86 @37 default
parent child Browse files
Show More
@@ -1,812 +1,814 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() or unwrapastype() to obtain the inner object.
35 35 """
36 36
37 37 __metaclass__ = abc.ABCMeta
38 38
39 39 @abc.abstractmethod
40 40 def contains(self, context, mapping, item):
41 41 """Test if the specified item is in self
42 42
43 43 The item argument may be a wrapped object.
44 44 """
45 45
46 46 @abc.abstractmethod
47 47 def getmember(self, context, mapping, key):
48 48 """Return a member item for the specified key
49 49
50 50 The key argument may be a wrapped object.
51 51 A returned object may be either a wrapped object or a pure value
52 52 depending on the self type.
53 53 """
54 54
55 55 @abc.abstractmethod
56 56 def getmin(self, context, mapping):
57 57 """Return the smallest item, which may be either a wrapped or a pure
58 58 value depending on the self type"""
59 59
60 60 @abc.abstractmethod
61 61 def getmax(self, context, mapping):
62 62 """Return the largest item, which may be either a wrapped or a pure
63 63 value depending on the self type"""
64 64
65 65 @abc.abstractmethod
66 66 def itermaps(self, context):
67 67 """Yield each template mapping"""
68 68
69 69 @abc.abstractmethod
70 70 def join(self, context, mapping, sep):
71 71 """Join items with the separator; Returns a bytes or (possibly nested)
72 72 generator of bytes
73 73
74 74 A pre-configured template may be rendered per item if this container
75 75 holds unprintable items.
76 76 """
77 77
78 78 @abc.abstractmethod
79 79 def show(self, context, mapping):
80 80 """Return a bytes or (possibly nested) generator of bytes representing
81 81 the underlying object
82 82
83 83 A pre-configured template may be rendered if the underlying object is
84 84 not printable.
85 85 """
86 86
87 87 @abc.abstractmethod
88 88 def tovalue(self, context, mapping):
89 89 """Move the inner value object out or create a value representation
90 90
91 91 A returned value must be serializable by templaterfilters.json().
92 92 """
93 93
94 94 class wrappedbytes(wrapped):
95 95 """Wrapper for byte string"""
96 96
97 97 def __init__(self, value):
98 98 self._value = value
99 99
100 100 def contains(self, context, mapping, item):
101 101 item = stringify(context, mapping, item)
102 102 return item in self._value
103 103
104 104 def getmember(self, context, mapping, key):
105 105 raise error.ParseError(_('%r is not a dictionary')
106 106 % pycompat.bytestr(self._value))
107 107
108 108 def getmin(self, context, mapping):
109 109 return self._getby(context, mapping, min)
110 110
111 111 def getmax(self, context, mapping):
112 112 return self._getby(context, mapping, max)
113 113
114 114 def _getby(self, context, mapping, func):
115 115 if not self._value:
116 116 raise error.ParseError(_('empty string'))
117 117 return func(pycompat.iterbytestr(self._value))
118 118
119 119 def itermaps(self, context):
120 120 raise error.ParseError(_('%r is not iterable of mappings')
121 121 % pycompat.bytestr(self._value))
122 122
123 123 def join(self, context, mapping, sep):
124 124 return joinitems(pycompat.iterbytestr(self._value), sep)
125 125
126 126 def show(self, context, mapping):
127 127 return self._value
128 128
129 129 def tovalue(self, context, mapping):
130 130 return self._value
131 131
132 132 class wrappedvalue(wrapped):
133 133 """Generic wrapper for pure non-list/dict/bytes value"""
134 134
135 135 def __init__(self, value):
136 136 self._value = value
137 137
138 138 def contains(self, context, mapping, item):
139 139 raise error.ParseError(_("%r is not iterable") % self._value)
140 140
141 141 def getmember(self, context, mapping, key):
142 142 raise error.ParseError(_('%r is not a dictionary') % self._value)
143 143
144 144 def getmin(self, context, mapping):
145 145 raise error.ParseError(_("%r is not iterable") % self._value)
146 146
147 147 def getmax(self, context, mapping):
148 148 raise error.ParseError(_("%r is not iterable") % self._value)
149 149
150 150 def itermaps(self, context):
151 151 raise error.ParseError(_('%r is not iterable of mappings')
152 152 % self._value)
153 153
154 154 def join(self, context, mapping, sep):
155 155 raise error.ParseError(_('%r is not iterable') % self._value)
156 156
157 157 def show(self, context, mapping):
158 if self._value is None:
159 return b''
158 160 return pycompat.bytestr(self._value)
159 161
160 162 def tovalue(self, context, mapping):
161 163 return self._value
162 164
163 165 # stub for representing a date type; may be a real date type that can
164 166 # provide a readable string value
165 167 class date(object):
166 168 pass
167 169
168 170 class hybrid(wrapped):
169 171 """Wrapper for list or dict to support legacy template
170 172
171 173 This class allows us to handle both:
172 174 - "{files}" (legacy command-line-specific list hack) and
173 175 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
174 176 and to access raw values:
175 177 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
176 178 - "{get(extras, key)}"
177 179 - "{files|json}"
178 180 """
179 181
180 182 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
181 183 self._gen = gen # generator or function returning generator
182 184 self._values = values
183 185 self._makemap = makemap
184 186 self._joinfmt = joinfmt
185 187 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
186 188
187 189 def contains(self, context, mapping, item):
188 190 item = unwrapastype(context, mapping, item, self._keytype)
189 191 return item in self._values
190 192
191 193 def getmember(self, context, mapping, key):
192 194 # TODO: maybe split hybrid list/dict types?
193 195 if not util.safehasattr(self._values, 'get'):
194 196 raise error.ParseError(_('not a dictionary'))
195 197 key = unwrapastype(context, mapping, key, self._keytype)
196 198 return self._wrapvalue(key, self._values.get(key))
197 199
198 200 def getmin(self, context, mapping):
199 201 return self._getby(context, mapping, min)
200 202
201 203 def getmax(self, context, mapping):
202 204 return self._getby(context, mapping, max)
203 205
204 206 def _getby(self, context, mapping, func):
205 207 if not self._values:
206 208 raise error.ParseError(_('empty sequence'))
207 209 val = func(self._values)
208 210 return self._wrapvalue(val, val)
209 211
210 212 def _wrapvalue(self, key, val):
211 213 if val is None:
212 214 return
213 215 if util.safehasattr(val, '_makemap'):
214 216 # a nested hybrid list/dict, which has its own way of map operation
215 217 return val
216 218 return mappable(None, key, val, self._makemap)
217 219
218 220 def itermaps(self, context):
219 221 makemap = self._makemap
220 222 for x in self._values:
221 223 yield makemap(x)
222 224
223 225 def join(self, context, mapping, sep):
224 226 # TODO: switch gen to (context, mapping) API?
225 227 return joinitems((self._joinfmt(x) for x in self._values), sep)
226 228
227 229 def show(self, context, mapping):
228 230 # TODO: switch gen to (context, mapping) API?
229 231 gen = self._gen
230 232 if gen is None:
231 233 return self.join(context, mapping, ' ')
232 234 if callable(gen):
233 235 return gen()
234 236 return gen
235 237
236 238 def tovalue(self, context, mapping):
237 239 # TODO: make it non-recursive for trivial lists/dicts
238 240 xs = self._values
239 241 if util.safehasattr(xs, 'get'):
240 242 return {k: unwrapvalue(context, mapping, v)
241 243 for k, v in xs.iteritems()}
242 244 return [unwrapvalue(context, mapping, x) for x in xs]
243 245
244 246 class mappable(wrapped):
245 247 """Wrapper for non-list/dict object to support map operation
246 248
247 249 This class allows us to handle both:
248 250 - "{manifest}"
249 251 - "{manifest % '{rev}:{node}'}"
250 252 - "{manifest.rev}"
251 253 """
252 254
253 255 def __init__(self, gen, key, value, makemap):
254 256 self._gen = gen # generator or function returning generator
255 257 self._key = key
256 258 self._value = value # may be generator of strings
257 259 self._makemap = makemap
258 260
259 261 def tomap(self):
260 262 return self._makemap(self._key)
261 263
262 264 def contains(self, context, mapping, item):
263 265 w = makewrapped(context, mapping, self._value)
264 266 return w.contains(context, mapping, item)
265 267
266 268 def getmember(self, context, mapping, key):
267 269 w = makewrapped(context, mapping, self._value)
268 270 return w.getmember(context, mapping, key)
269 271
270 272 def getmin(self, context, mapping):
271 273 w = makewrapped(context, mapping, self._value)
272 274 return w.getmin(context, mapping)
273 275
274 276 def getmax(self, context, mapping):
275 277 w = makewrapped(context, mapping, self._value)
276 278 return w.getmax(context, mapping)
277 279
278 280 def itermaps(self, context):
279 281 yield self.tomap()
280 282
281 283 def join(self, context, mapping, sep):
282 284 w = makewrapped(context, mapping, self._value)
283 285 return w.join(context, mapping, sep)
284 286
285 287 def show(self, context, mapping):
286 288 # TODO: switch gen to (context, mapping) API?
287 289 gen = self._gen
288 290 if gen is None:
289 291 return pycompat.bytestr(self._value)
290 292 if callable(gen):
291 293 return gen()
292 294 return gen
293 295
294 296 def tovalue(self, context, mapping):
295 297 return _unthunk(context, mapping, self._value)
296 298
297 299 class _mappingsequence(wrapped):
298 300 """Wrapper for sequence of template mappings
299 301
300 302 This represents an inner template structure (i.e. a list of dicts),
301 303 which can also be rendered by the specified named/literal template.
302 304
303 305 Template mappings may be nested.
304 306 """
305 307
306 308 def __init__(self, name=None, tmpl=None, sep=''):
307 309 if name is not None and tmpl is not None:
308 310 raise error.ProgrammingError('name and tmpl are mutually exclusive')
309 311 self._name = name
310 312 self._tmpl = tmpl
311 313 self._defaultsep = sep
312 314
313 315 def contains(self, context, mapping, item):
314 316 raise error.ParseError(_('not comparable'))
315 317
316 318 def getmember(self, context, mapping, key):
317 319 raise error.ParseError(_('not a dictionary'))
318 320
319 321 def getmin(self, context, mapping):
320 322 raise error.ParseError(_('not comparable'))
321 323
322 324 def getmax(self, context, mapping):
323 325 raise error.ParseError(_('not comparable'))
324 326
325 327 def join(self, context, mapping, sep):
326 328 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
327 329 if self._name:
328 330 itemiter = (context.process(self._name, m) for m in mapsiter)
329 331 elif self._tmpl:
330 332 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
331 333 else:
332 334 raise error.ParseError(_('not displayable without template'))
333 335 return joinitems(itemiter, sep)
334 336
335 337 def show(self, context, mapping):
336 338 return self.join(context, mapping, self._defaultsep)
337 339
338 340 def tovalue(self, context, mapping):
339 341 knownres = context.knownresourcekeys()
340 342 items = []
341 343 for nm in self.itermaps(context):
342 344 # drop internal resources (recursively) which shouldn't be displayed
343 345 lm = context.overlaymap(mapping, nm)
344 346 items.append({k: unwrapvalue(context, lm, v)
345 347 for k, v in nm.iteritems() if k not in knownres})
346 348 return items
347 349
348 350 class mappinggenerator(_mappingsequence):
349 351 """Wrapper for generator of template mappings
350 352
351 353 The function ``make(context, *args)`` should return a generator of
352 354 mapping dicts.
353 355 """
354 356
355 357 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
356 358 super(mappinggenerator, self).__init__(name, tmpl, sep)
357 359 self._make = make
358 360 self._args = args
359 361
360 362 def itermaps(self, context):
361 363 return self._make(context, *self._args)
362 364
363 365 class mappinglist(_mappingsequence):
364 366 """Wrapper for list of template mappings"""
365 367
366 368 def __init__(self, mappings, name=None, tmpl=None, sep=''):
367 369 super(mappinglist, self).__init__(name, tmpl, sep)
368 370 self._mappings = mappings
369 371
370 372 def itermaps(self, context):
371 373 return iter(self._mappings)
372 374
373 375 class mappedgenerator(wrapped):
374 376 """Wrapper for generator of strings which acts as a list
375 377
376 378 The function ``make(context, *args)`` should return a generator of
377 379 byte strings, or a generator of (possibly nested) generators of byte
378 380 strings (i.e. a generator for a list of byte strings.)
379 381 """
380 382
381 383 def __init__(self, make, args=()):
382 384 self._make = make
383 385 self._args = args
384 386
385 387 def contains(self, context, mapping, item):
386 388 item = stringify(context, mapping, item)
387 389 return item in self.tovalue(context, mapping)
388 390
389 391 def _gen(self, context):
390 392 return self._make(context, *self._args)
391 393
392 394 def getmember(self, context, mapping, key):
393 395 raise error.ParseError(_('not a dictionary'))
394 396
395 397 def getmin(self, context, mapping):
396 398 return self._getby(context, mapping, min)
397 399
398 400 def getmax(self, context, mapping):
399 401 return self._getby(context, mapping, max)
400 402
401 403 def _getby(self, context, mapping, func):
402 404 xs = self.tovalue(context, mapping)
403 405 if not xs:
404 406 raise error.ParseError(_('empty sequence'))
405 407 return func(xs)
406 408
407 409 def itermaps(self, context):
408 410 raise error.ParseError(_('list of strings is not mappable'))
409 411
410 412 def join(self, context, mapping, sep):
411 413 return joinitems(self._gen(context), sep)
412 414
413 415 def show(self, context, mapping):
414 416 return self.join(context, mapping, '')
415 417
416 418 def tovalue(self, context, mapping):
417 419 return [stringify(context, mapping, x) for x in self._gen(context)]
418 420
419 421 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
420 422 """Wrap data to support both dict-like and string-like operations"""
421 423 prefmt = pycompat.identity
422 424 if fmt is None:
423 425 fmt = '%s=%s'
424 426 prefmt = pycompat.bytestr
425 427 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
426 428 lambda k: fmt % (prefmt(k), prefmt(data[k])))
427 429
428 430 def hybridlist(data, name, fmt=None, gen=None):
429 431 """Wrap data to support both list-like and string-like operations"""
430 432 prefmt = pycompat.identity
431 433 if fmt is None:
432 434 fmt = '%s'
433 435 prefmt = pycompat.bytestr
434 436 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
435 437
436 438 def compatdict(context, mapping, name, data, key='key', value='value',
437 439 fmt=None, plural=None, separator=' '):
438 440 """Wrap data like hybriddict(), but also supports old-style list template
439 441
440 442 This exists for backward compatibility with the old-style template. Use
441 443 hybriddict() for new template keywords.
442 444 """
443 445 c = [{key: k, value: v} for k, v in data.iteritems()]
444 446 f = _showcompatlist(context, mapping, name, c, plural, separator)
445 447 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
446 448
447 449 def compatlist(context, mapping, name, data, element=None, fmt=None,
448 450 plural=None, separator=' '):
449 451 """Wrap data like hybridlist(), but also supports old-style list template
450 452
451 453 This exists for backward compatibility with the old-style template. Use
452 454 hybridlist() for new template keywords.
453 455 """
454 456 f = _showcompatlist(context, mapping, name, data, plural, separator)
455 457 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
456 458
457 459 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
458 460 """Return a generator that renders old-style list template
459 461
460 462 name is name of key in template map.
461 463 values is list of strings or dicts.
462 464 plural is plural of name, if not simply name + 's'.
463 465 separator is used to join values as a string
464 466
465 467 expansion works like this, given name 'foo'.
466 468
467 469 if values is empty, expand 'no_foos'.
468 470
469 471 if 'foo' not in template map, return values as a string,
470 472 joined by 'separator'.
471 473
472 474 expand 'start_foos'.
473 475
474 476 for each value, expand 'foo'. if 'last_foo' in template
475 477 map, expand it instead of 'foo' for last key.
476 478
477 479 expand 'end_foos'.
478 480 """
479 481 if not plural:
480 482 plural = name + 's'
481 483 if not values:
482 484 noname = 'no_' + plural
483 485 if context.preload(noname):
484 486 yield context.process(noname, mapping)
485 487 return
486 488 if not context.preload(name):
487 489 if isinstance(values[0], bytes):
488 490 yield separator.join(values)
489 491 else:
490 492 for v in values:
491 493 r = dict(v)
492 494 r.update(mapping)
493 495 yield r
494 496 return
495 497 startname = 'start_' + plural
496 498 if context.preload(startname):
497 499 yield context.process(startname, mapping)
498 500 def one(v, tag=name):
499 501 vmapping = {}
500 502 try:
501 503 vmapping.update(v)
502 504 # Python 2 raises ValueError if the type of v is wrong. Python
503 505 # 3 raises TypeError.
504 506 except (AttributeError, TypeError, ValueError):
505 507 try:
506 508 # Python 2 raises ValueError trying to destructure an e.g.
507 509 # bytes. Python 3 raises TypeError.
508 510 for a, b in v:
509 511 vmapping[a] = b
510 512 except (TypeError, ValueError):
511 513 vmapping[name] = v
512 514 vmapping = context.overlaymap(mapping, vmapping)
513 515 return context.process(tag, vmapping)
514 516 lastname = 'last_' + name
515 517 if context.preload(lastname):
516 518 last = values.pop()
517 519 else:
518 520 last = None
519 521 for v in values:
520 522 yield one(v)
521 523 if last is not None:
522 524 yield one(last, tag=lastname)
523 525 endname = 'end_' + plural
524 526 if context.preload(endname):
525 527 yield context.process(endname, mapping)
526 528
527 529 def flatten(context, mapping, thing):
528 530 """Yield a single stream from a possibly nested set of iterators"""
529 531 if isinstance(thing, wrapped):
530 532 thing = thing.show(context, mapping)
531 533 if isinstance(thing, bytes):
532 534 yield thing
533 535 elif isinstance(thing, str):
534 536 # We can only hit this on Python 3, and it's here to guard
535 537 # against infinite recursion.
536 538 raise error.ProgrammingError('Mercurial IO including templates is done'
537 539 ' with bytes, not strings, got %r' % thing)
538 540 elif thing is None:
539 541 pass
540 542 elif not util.safehasattr(thing, '__iter__'):
541 543 yield pycompat.bytestr(thing)
542 544 else:
543 545 for i in thing:
544 546 if isinstance(i, wrapped):
545 547 i = i.show(context, mapping)
546 548 if isinstance(i, bytes):
547 549 yield i
548 550 elif i is None:
549 551 pass
550 552 elif not util.safehasattr(i, '__iter__'):
551 553 yield pycompat.bytestr(i)
552 554 else:
553 555 for j in flatten(context, mapping, i):
554 556 yield j
555 557
556 558 def stringify(context, mapping, thing):
557 559 """Turn values into bytes by converting into text and concatenating them"""
558 560 if isinstance(thing, bytes):
559 561 return thing # retain localstr to be round-tripped
560 562 return b''.join(flatten(context, mapping, thing))
561 563
562 564 def findsymbolicname(arg):
563 565 """Find symbolic name for the given compiled expression; returns None
564 566 if nothing found reliably"""
565 567 while True:
566 568 func, data = arg
567 569 if func is runsymbol:
568 570 return data
569 571 elif func is runfilter:
570 572 arg = data[0]
571 573 else:
572 574 return None
573 575
574 576 def _unthunk(context, mapping, thing):
575 577 """Evaluate a lazy byte string into value"""
576 578 if not isinstance(thing, types.GeneratorType):
577 579 return thing
578 580 return stringify(context, mapping, thing)
579 581
580 582 def evalrawexp(context, mapping, arg):
581 583 """Evaluate given argument as a bare template object which may require
582 584 further processing (such as folding generator of strings)"""
583 585 func, data = arg
584 586 return func(context, mapping, data)
585 587
586 588 def evalwrapped(context, mapping, arg):
587 589 """Evaluate given argument to wrapped object"""
588 590 thing = evalrawexp(context, mapping, arg)
589 591 return makewrapped(context, mapping, thing)
590 592
591 593 def makewrapped(context, mapping, thing):
592 594 """Lift object to a wrapped type"""
593 595 if isinstance(thing, wrapped):
594 596 return thing
595 597 thing = _unthunk(context, mapping, thing)
596 598 if isinstance(thing, bytes):
597 599 return wrappedbytes(thing)
598 600 return wrappedvalue(thing)
599 601
600 602 def evalfuncarg(context, mapping, arg):
601 603 """Evaluate given argument as value type"""
602 604 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
603 605
604 606 def unwrapvalue(context, mapping, thing):
605 607 """Move the inner value object out of the wrapper"""
606 608 if isinstance(thing, wrapped):
607 609 return thing.tovalue(context, mapping)
608 610 # evalrawexp() may return string, generator of strings or arbitrary object
609 611 # such as date tuple, but filter does not want generator.
610 612 return _unthunk(context, mapping, thing)
611 613
612 614 def evalboolean(context, mapping, arg):
613 615 """Evaluate given argument as boolean, but also takes boolean literals"""
614 616 func, data = arg
615 617 if func is runsymbol:
616 618 thing = func(context, mapping, data, default=None)
617 619 if thing is None:
618 620 # not a template keyword, takes as a boolean literal
619 621 thing = stringutil.parsebool(data)
620 622 else:
621 623 thing = func(context, mapping, data)
622 624 if isinstance(thing, wrapped):
623 625 thing = thing.tovalue(context, mapping)
624 626 if isinstance(thing, bool):
625 627 return thing
626 628 # other objects are evaluated as strings, which means 0 is True, but
627 629 # empty dict/list should be False as they are expected to be ''
628 630 return bool(stringify(context, mapping, thing))
629 631
630 632 def evaldate(context, mapping, arg, err=None):
631 633 """Evaluate given argument as a date tuple or a date string; returns
632 634 a (unixtime, offset) tuple"""
633 635 thing = evalrawexp(context, mapping, arg)
634 636 return unwrapdate(context, mapping, thing, err)
635 637
636 638 def unwrapdate(context, mapping, thing, err=None):
637 639 thing = unwrapvalue(context, mapping, thing)
638 640 try:
639 641 return dateutil.parsedate(thing)
640 642 except AttributeError:
641 643 raise error.ParseError(err or _('not a date tuple nor a string'))
642 644 except error.ParseError:
643 645 if not err:
644 646 raise
645 647 raise error.ParseError(err)
646 648
647 649 def evalinteger(context, mapping, arg, err=None):
648 650 thing = evalrawexp(context, mapping, arg)
649 651 return unwrapinteger(context, mapping, thing, err)
650 652
651 653 def unwrapinteger(context, mapping, thing, err=None):
652 654 thing = unwrapvalue(context, mapping, thing)
653 655 try:
654 656 return int(thing)
655 657 except (TypeError, ValueError):
656 658 raise error.ParseError(err or _('not an integer'))
657 659
658 660 def evalstring(context, mapping, arg):
659 661 return stringify(context, mapping, evalrawexp(context, mapping, arg))
660 662
661 663 def evalstringliteral(context, mapping, arg):
662 664 """Evaluate given argument as string template, but returns symbol name
663 665 if it is unknown"""
664 666 func, data = arg
665 667 if func is runsymbol:
666 668 thing = func(context, mapping, data, default=data)
667 669 else:
668 670 thing = func(context, mapping, data)
669 671 return stringify(context, mapping, thing)
670 672
671 673 _unwrapfuncbytype = {
672 674 None: unwrapvalue,
673 675 bytes: stringify,
674 676 date: unwrapdate,
675 677 int: unwrapinteger,
676 678 }
677 679
678 680 def unwrapastype(context, mapping, thing, typ):
679 681 """Move the inner value object out of the wrapper and coerce its type"""
680 682 try:
681 683 f = _unwrapfuncbytype[typ]
682 684 except KeyError:
683 685 raise error.ProgrammingError('invalid type specified: %r' % typ)
684 686 return f(context, mapping, thing)
685 687
686 688 def runinteger(context, mapping, data):
687 689 return int(data)
688 690
689 691 def runstring(context, mapping, data):
690 692 return data
691 693
692 694 def _recursivesymbolblocker(key):
693 695 def showrecursion(**args):
694 696 raise error.Abort(_("recursive reference '%s' in template") % key)
695 697 return showrecursion
696 698
697 699 def runsymbol(context, mapping, key, default=''):
698 700 v = context.symbol(mapping, key)
699 701 if v is None:
700 702 # put poison to cut recursion. we can't move this to parsing phase
701 703 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
702 704 safemapping = mapping.copy()
703 705 safemapping[key] = _recursivesymbolblocker(key)
704 706 try:
705 707 v = context.process(key, safemapping)
706 708 except TemplateNotFound:
707 709 v = default
708 710 if callable(v) and getattr(v, '_requires', None) is None:
709 711 # old templatekw: expand all keywords and resources
710 712 # (TODO: deprecate this after porting web template keywords to new API)
711 713 props = {k: context._resources.lookup(context, mapping, k)
712 714 for k in context._resources.knownkeys()}
713 715 # pass context to _showcompatlist() through templatekw._showlist()
714 716 props['templ'] = context
715 717 props.update(mapping)
716 718 return v(**pycompat.strkwargs(props))
717 719 if callable(v):
718 720 # new templatekw
719 721 try:
720 722 return v(context, mapping)
721 723 except ResourceUnavailable:
722 724 # unsupported keyword is mapped to empty just like unknown keyword
723 725 return None
724 726 return v
725 727
726 728 def runtemplate(context, mapping, template):
727 729 for arg in template:
728 730 yield evalrawexp(context, mapping, arg)
729 731
730 732 def runfilter(context, mapping, data):
731 733 arg, filt = data
732 734 thing = evalrawexp(context, mapping, arg)
733 735 intype = getattr(filt, '_intype', None)
734 736 try:
735 737 thing = unwrapastype(context, mapping, thing, intype)
736 738 return filt(thing)
737 739 except error.ParseError as e:
738 740 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
739 741
740 742 def _formatfiltererror(arg, filt):
741 743 fn = pycompat.sysbytes(filt.__name__)
742 744 sym = findsymbolicname(arg)
743 745 if not sym:
744 746 return _("incompatible use of template filter '%s'") % fn
745 747 return (_("template filter '%s' is not compatible with keyword '%s'")
746 748 % (fn, sym))
747 749
748 750 def _iteroverlaymaps(context, origmapping, newmappings):
749 751 """Generate combined mappings from the original mapping and an iterable
750 752 of partial mappings to override the original"""
751 753 for i, nm in enumerate(newmappings):
752 754 lm = context.overlaymap(origmapping, nm)
753 755 lm['index'] = i
754 756 yield lm
755 757
756 758 def _applymap(context, mapping, d, darg, targ):
757 759 try:
758 760 diter = d.itermaps(context)
759 761 except error.ParseError as err:
760 762 sym = findsymbolicname(darg)
761 763 if not sym:
762 764 raise
763 765 hint = _("keyword '%s' does not support map operation") % sym
764 766 raise error.ParseError(bytes(err), hint=hint)
765 767 for lm in _iteroverlaymaps(context, mapping, diter):
766 768 yield evalrawexp(context, lm, targ)
767 769
768 770 def runmap(context, mapping, data):
769 771 darg, targ = data
770 772 d = evalwrapped(context, mapping, darg)
771 773 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
772 774
773 775 def runmember(context, mapping, data):
774 776 darg, memb = data
775 777 d = evalwrapped(context, mapping, darg)
776 778 if util.safehasattr(d, 'tomap'):
777 779 lm = context.overlaymap(mapping, d.tomap())
778 780 return runsymbol(context, lm, memb)
779 781 try:
780 782 return d.getmember(context, mapping, memb)
781 783 except error.ParseError as err:
782 784 sym = findsymbolicname(darg)
783 785 if not sym:
784 786 raise
785 787 hint = _("keyword '%s' does not support member operation") % sym
786 788 raise error.ParseError(bytes(err), hint=hint)
787 789
788 790 def runnegate(context, mapping, data):
789 791 data = evalinteger(context, mapping, data,
790 792 _('negation needs an integer argument'))
791 793 return -data
792 794
793 795 def runarithmetic(context, mapping, data):
794 796 func, left, right = data
795 797 left = evalinteger(context, mapping, left,
796 798 _('arithmetic only defined on integers'))
797 799 right = evalinteger(context, mapping, right,
798 800 _('arithmetic only defined on integers'))
799 801 try:
800 802 return func(left, right)
801 803 except ZeroDivisionError:
802 804 raise error.Abort(_('division by zero is not defined'))
803 805
804 806 def joinitems(itemiter, sep):
805 807 """Join items with the separator; Returns generator of bytes"""
806 808 first = True
807 809 for x in itemiter:
808 810 if first:
809 811 first = False
810 812 elif sep:
811 813 yield sep
812 814 yield x
General Comments 0
You need to be logged in to leave comments. Login now