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