1# templateutil.py - utility for template evaluation 2# 3# Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import abc 11import types 12 13from .i18n import _ 14from .pycompat import getattr 15from . import ( 16 error, 17 pycompat, 18 smartset, 19 util, 20) 21from .utils import ( 22 dateutil, 23 stringutil, 24) 25 26 27class ResourceUnavailable(error.Abort): 28 pass 29 30 31class TemplateNotFound(error.Abort): 32 pass 33 34 35class wrapped(object): # pytype: disable=ignored-metaclass 36 """Object requiring extra conversion prior to displaying or processing 37 as value 38 39 Use unwrapvalue() or unwrapastype() to obtain the inner object. 40 """ 41 42 __metaclass__ = abc.ABCMeta 43 44 @abc.abstractmethod 45 def contains(self, context, mapping, item): 46 """Test if the specified item is in self 47 48 The item argument may be a wrapped object. 49 """ 50 51 @abc.abstractmethod 52 def getmember(self, context, mapping, key): 53 """Return a member item for the specified key 54 55 The key argument may be a wrapped object. 56 A returned object may be either a wrapped object or a pure value 57 depending on the self type. 58 """ 59 60 @abc.abstractmethod 61 def getmin(self, context, mapping): 62 """Return the smallest item, which may be either a wrapped or a pure 63 value depending on the self type""" 64 65 @abc.abstractmethod 66 def getmax(self, context, mapping): 67 """Return the largest item, which may be either a wrapped or a pure 68 value depending on the self type""" 69 70 @abc.abstractmethod 71 def filter(self, context, mapping, select): 72 """Return new container of the same type which includes only the 73 selected elements 74 75 select() takes each item as a wrapped object and returns True/False. 76 """ 77 78 @abc.abstractmethod 79 def itermaps(self, context): 80 """Yield each template mapping""" 81 82 @abc.abstractmethod 83 def join(self, context, mapping, sep): 84 """Join items with the separator; Returns a bytes or (possibly nested) 85 generator of bytes 86 87 A pre-configured template may be rendered per item if this container 88 holds unprintable items. 89 """ 90 91 @abc.abstractmethod 92 def show(self, context, mapping): 93 """Return a bytes or (possibly nested) generator of bytes representing 94 the underlying object 95 96 A pre-configured template may be rendered if the underlying object is 97 not printable. 98 """ 99 100 @abc.abstractmethod 101 def tobool(self, context, mapping): 102 """Return a boolean representation of the inner value""" 103 104 @abc.abstractmethod 105 def tovalue(self, context, mapping): 106 """Move the inner value object out or create a value representation 107 108 A returned value must be serializable by templaterfilters.json(). 109 """ 110 111 112class mappable(object): # pytype: disable=ignored-metaclass 113 """Object which can be converted to a single template mapping""" 114 115 __metaclass__ = abc.ABCMeta 116 117 def itermaps(self, context): 118 yield self.tomap(context) 119 120 @abc.abstractmethod 121 def tomap(self, context): 122 """Create a single template mapping representing this""" 123 124 125class wrappedbytes(wrapped): 126 """Wrapper for byte string""" 127 128 def __init__(self, value): 129 self._value = value 130 131 def contains(self, context, mapping, item): 132 item = stringify(context, mapping, item) 133 return item in self._value 134 135 def getmember(self, context, mapping, key): 136 raise error.ParseError( 137 _(b'%r is not a dictionary') % pycompat.bytestr(self._value) 138 ) 139 140 def getmin(self, context, mapping): 141 return self._getby(context, mapping, min) 142 143 def getmax(self, context, mapping): 144 return self._getby(context, mapping, max) 145 146 def _getby(self, context, mapping, func): 147 if not self._value: 148 raise error.ParseError(_(b'empty string')) 149 return func(pycompat.iterbytestr(self._value)) 150 151 def filter(self, context, mapping, select): 152 raise error.ParseError( 153 _(b'%r is not filterable') % pycompat.bytestr(self._value) 154 ) 155 156 def itermaps(self, context): 157 raise error.ParseError( 158 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value) 159 ) 160 161 def join(self, context, mapping, sep): 162 return joinitems(pycompat.iterbytestr(self._value), sep) 163 164 def show(self, context, mapping): 165 return self._value 166 167 def tobool(self, context, mapping): 168 return bool(self._value) 169 170 def tovalue(self, context, mapping): 171 return self._value 172 173 174class wrappedvalue(wrapped): 175 """Generic wrapper for pure non-list/dict/bytes value""" 176 177 def __init__(self, value): 178 self._value = value 179 180 def contains(self, context, mapping, item): 181 raise error.ParseError(_(b"%r is not iterable") % self._value) 182 183 def getmember(self, context, mapping, key): 184 raise error.ParseError(_(b'%r is not a dictionary') % self._value) 185 186 def getmin(self, context, mapping): 187 raise error.ParseError(_(b"%r is not iterable") % self._value) 188 189 def getmax(self, context, mapping): 190 raise error.ParseError(_(b"%r is not iterable") % self._value) 191 192 def filter(self, context, mapping, select): 193 raise error.ParseError(_(b"%r is not iterable") % self._value) 194 195 def itermaps(self, context): 196 raise error.ParseError( 197 _(b'%r is not iterable of mappings') % self._value 198 ) 199 200 def join(self, context, mapping, sep): 201 raise error.ParseError(_(b'%r is not iterable') % self._value) 202 203 def show(self, context, mapping): 204 if self._value is None: 205 return b'' 206 return pycompat.bytestr(self._value) 207 208 def tobool(self, context, mapping): 209 if self._value is None: 210 return False 211 if isinstance(self._value, bool): 212 return self._value 213 # otherwise evaluate as string, which means 0 is True 214 return bool(pycompat.bytestr(self._value)) 215 216 def tovalue(self, context, mapping): 217 return self._value 218 219 220class date(mappable, wrapped): 221 """Wrapper for date tuple""" 222 223 def __init__(self, value, showfmt=b'%d %d'): 224 # value may be (float, int), but public interface shouldn't support 225 # floating-point timestamp 226 self._unixtime, self._tzoffset = map(int, value) 227 self._showfmt = showfmt 228 229 def contains(self, context, mapping, item): 230 raise error.ParseError(_(b'date is not iterable')) 231 232 def getmember(self, context, mapping, key): 233 raise error.ParseError(_(b'date is not a dictionary')) 234 235 def getmin(self, context, mapping): 236 raise error.ParseError(_(b'date is not iterable')) 237 238 def getmax(self, context, mapping): 239 raise error.ParseError(_(b'date is not iterable')) 240 241 def filter(self, context, mapping, select): 242 raise error.ParseError(_(b'date is not iterable')) 243 244 def join(self, context, mapping, sep): 245 raise error.ParseError(_(b"date is not iterable")) 246 247 def show(self, context, mapping): 248 return self._showfmt % (self._unixtime, self._tzoffset) 249 250 def tomap(self, context): 251 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset} 252 253 def tobool(self, context, mapping): 254 return True 255 256 def tovalue(self, context, mapping): 257 return (self._unixtime, self._tzoffset) 258 259 260class hybrid(wrapped): 261 """Wrapper for list or dict to support legacy template 262 263 This class allows us to handle both: 264 - "{files}" (legacy command-line-specific list hack) and 265 - "{files % '{file}\n'}" (hgweb-style with inlining and function support) 266 and to access raw values: 267 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}" 268 - "{get(extras, key)}" 269 - "{files|json}" 270 """ 271 272 def __init__(self, gen, values, makemap, joinfmt, keytype=None): 273 self._gen = gen # generator or function returning generator 274 self._values = values 275 self._makemap = makemap 276 self._joinfmt = joinfmt 277 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved 278 279 def contains(self, context, mapping, item): 280 item = unwrapastype(context, mapping, item, self._keytype) 281 return item in self._values 282 283 def getmember(self, context, mapping, key): 284 # TODO: maybe split hybrid list/dict types? 285 if not util.safehasattr(self._values, b'get'): 286 raise error.ParseError(_(b'not a dictionary')) 287 key = unwrapastype(context, mapping, key, self._keytype) 288 return self._wrapvalue(key, self._values.get(key)) 289 290 def getmin(self, context, mapping): 291 return self._getby(context, mapping, min) 292 293 def getmax(self, context, mapping): 294 return self._getby(context, mapping, max) 295 296 def _getby(self, context, mapping, func): 297 if not self._values: 298 raise error.ParseError(_(b'empty sequence')) 299 val = func(self._values) 300 return self._wrapvalue(val, val) 301 302 def _wrapvalue(self, key, val): 303 if val is None: 304 return 305 if util.safehasattr(val, b'_makemap'): 306 # a nested hybrid list/dict, which has its own way of map operation 307 return val 308 return hybriditem(None, key, val, self._makemap) 309 310 def filter(self, context, mapping, select): 311 if util.safehasattr(self._values, b'get'): 312 values = { 313 k: v 314 for k, v in pycompat.iteritems(self._values) 315 if select(self._wrapvalue(k, v)) 316 } 317 else: 318 values = [v for v in self._values if select(self._wrapvalue(v, v))] 319 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype) 320 321 def itermaps(self, context): 322 makemap = self._makemap 323 for x in self._values: 324 yield makemap(x) 325 326 def join(self, context, mapping, sep): 327 # TODO: switch gen to (context, mapping) API? 328 return joinitems((self._joinfmt(x) for x in self._values), sep) 329 330 def show(self, context, mapping): 331 # TODO: switch gen to (context, mapping) API? 332 gen = self._gen 333 if gen is None: 334 return self.join(context, mapping, b' ') 335 if callable(gen): 336 return gen() 337 return gen 338 339 def tobool(self, context, mapping): 340 return bool(self._values) 341 342 def tovalue(self, context, mapping): 343 # TODO: make it non-recursive for trivial lists/dicts 344 xs = self._values 345 if util.safehasattr(xs, b'get'): 346 return { 347 k: unwrapvalue(context, mapping, v) 348 for k, v in pycompat.iteritems(xs) 349 } 350 return [unwrapvalue(context, mapping, x) for x in xs] 351 352 353class hybriditem(mappable, wrapped): 354 """Wrapper for non-list/dict object to support map operation 355 356 This class allows us to handle both: 357 - "{manifest}" 358 - "{manifest % '{rev}:{node}'}" 359 - "{manifest.rev}" 360 """ 361 362 def __init__(self, gen, key, value, makemap): 363 self._gen = gen # generator or function returning generator 364 self._key = key 365 self._value = value # may be generator of strings 366 self._makemap = makemap 367 368 def tomap(self, context): 369 return self._makemap(self._key) 370 371 def contains(self, context, mapping, item): 372 w = makewrapped(context, mapping, self._value) 373 return w.contains(context, mapping, item) 374 375 def getmember(self, context, mapping, key): 376 w = makewrapped(context, mapping, self._value) 377 return w.getmember(context, mapping, key) 378 379 def getmin(self, context, mapping): 380 w = makewrapped(context, mapping, self._value) 381 return w.getmin(context, mapping) 382 383 def getmax(self, context, mapping): 384 w = makewrapped(context, mapping, self._value) 385 return w.getmax(context, mapping) 386 387 def filter(self, context, mapping, select): 388 w = makewrapped(context, mapping, self._value) 389 return w.filter(context, mapping, select) 390 391 def join(self, context, mapping, sep): 392 w = makewrapped(context, mapping, self._value) 393 return w.join(context, mapping, sep) 394 395 def show(self, context, mapping): 396 # TODO: switch gen to (context, mapping) API? 397 gen = self._gen 398 if gen is None: 399 return pycompat.bytestr(self._value) 400 if callable(gen): 401 return gen() 402 return gen 403 404 def tobool(self, context, mapping): 405 w = makewrapped(context, mapping, self._value) 406 return w.tobool(context, mapping) 407 408 def tovalue(self, context, mapping): 409 return _unthunk(context, mapping, self._value) 410 411 412class revslist(wrapped): 413 """Wrapper for a smartset (a list/set of revision numbers) 414 415 If name specified, the revs will be rendered with the old-style list 416 template of the given name by default. 417 418 The cachekey provides a hint to cache further computation on this 419 smartset. If the underlying smartset is dynamically created, the cachekey 420 should be None. 421 """ 422 423 def __init__(self, repo, revs, name=None, cachekey=None): 424 assert isinstance(revs, smartset.abstractsmartset) 425 self._repo = repo 426 self._revs = revs 427 self._name = name 428 self.cachekey = cachekey 429 430 def contains(self, context, mapping, item): 431 rev = unwrapinteger(context, mapping, item) 432 return rev in self._revs 433 434 def getmember(self, context, mapping, key): 435 raise error.ParseError(_(b'not a dictionary')) 436 437 def getmin(self, context, mapping): 438 makehybriditem = self._makehybriditemfunc() 439 return makehybriditem(self._revs.min()) 440 441 def getmax(self, context, mapping): 442 makehybriditem = self._makehybriditemfunc() 443 return makehybriditem(self._revs.max()) 444 445 def filter(self, context, mapping, select): 446 makehybriditem = self._makehybriditemfunc() 447 frevs = self._revs.filter(lambda r: select(makehybriditem(r))) 448 # once filtered, no need to support old-style list template 449 return revslist(self._repo, frevs, name=None) 450 451 def itermaps(self, context): 452 makemap = self._makemapfunc() 453 for r in self._revs: 454 yield makemap(r) 455 456 def _makehybriditemfunc(self): 457 makemap = self._makemapfunc() 458 return lambda r: hybriditem(None, r, r, makemap) 459 460 def _makemapfunc(self): 461 repo = self._repo 462 name = self._name 463 if name: 464 return lambda r: {name: r, b'ctx': repo[r]} 465 else: 466 return lambda r: {b'ctx': repo[r]} 467 468 def join(self, context, mapping, sep): 469 return joinitems(self._revs, sep) 470 471 def show(self, context, mapping): 472 if self._name: 473 srevs = [b'%d' % r for r in self._revs] 474 return _showcompatlist(context, mapping, self._name, srevs) 475 else: 476 return self.join(context, mapping, b' ') 477 478 def tobool(self, context, mapping): 479 return bool(self._revs) 480 481 def tovalue(self, context, mapping): 482 return self._revs 483 484 485class _mappingsequence(wrapped): 486 """Wrapper for sequence of template mappings 487 488 This represents an inner template structure (i.e. a list of dicts), 489 which can also be rendered by the specified named/literal template. 490 491 Template mappings may be nested. 492 """ 493 494 def __init__(self, name=None, tmpl=None, sep=b''): 495 if name is not None and tmpl is not None: 496 raise error.ProgrammingError( 497 b'name and tmpl are mutually exclusive' 498 ) 499 self._name = name 500 self._tmpl = tmpl 501 self._defaultsep = sep 502 503 def contains(self, context, mapping, item): 504 raise error.ParseError(_(b'not comparable')) 505 506 def getmember(self, context, mapping, key): 507 raise error.ParseError(_(b'not a dictionary')) 508 509 def getmin(self, context, mapping): 510 raise error.ParseError(_(b'not comparable')) 511 512 def getmax(self, context, mapping): 513 raise error.ParseError(_(b'not comparable')) 514 515 def filter(self, context, mapping, select): 516 # implement if necessary; we'll need a wrapped type for a mapping dict 517 raise error.ParseError(_(b'not filterable without template')) 518 519 def join(self, context, mapping, sep): 520 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context)) 521 if self._name: 522 itemiter = (context.process(self._name, m) for m in mapsiter) 523 elif self._tmpl: 524 itemiter = (context.expand(self._tmpl, m) for m in mapsiter) 525 else: 526 raise error.ParseError(_(b'not displayable without template')) 527 return joinitems(itemiter, sep) 528 529 def show(self, context, mapping): 530 return self.join(context, mapping, self._defaultsep) 531 532 def tovalue(self, context, mapping): 533 knownres = context.knownresourcekeys() 534 items = [] 535 for nm in self.itermaps(context): 536 # drop internal resources (recursively) which shouldn't be displayed 537 lm = context.overlaymap(mapping, nm) 538 items.append( 539 { 540 k: unwrapvalue(context, lm, v) 541 for k, v in pycompat.iteritems(nm) 542 if k not in knownres 543 } 544 ) 545 return items 546 547 548class mappinggenerator(_mappingsequence): 549 """Wrapper for generator of template mappings 550 551 The function ``make(context, *args)`` should return a generator of 552 mapping dicts. 553 """ 554 555 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''): 556 super(mappinggenerator, self).__init__(name, tmpl, sep) 557 self._make = make 558 self._args = args 559 560 def itermaps(self, context): 561 return self._make(context, *self._args) 562 563 def tobool(self, context, mapping): 564 return _nonempty(self.itermaps(context)) 565 566 567class mappinglist(_mappingsequence): 568 """Wrapper for list of template mappings""" 569 570 def __init__(self, mappings, name=None, tmpl=None, sep=b''): 571 super(mappinglist, self).__init__(name, tmpl, sep) 572 self._mappings = mappings 573 574 def itermaps(self, context): 575 return iter(self._mappings) 576 577 def tobool(self, context, mapping): 578 return bool(self._mappings) 579 580 581class mappingdict(mappable, _mappingsequence): 582 """Wrapper for a single template mapping 583 584 This isn't a sequence in a way that the underlying dict won't be iterated 585 as a dict, but shares most of the _mappingsequence functions. 586 """ 587 588 def __init__(self, mapping, name=None, tmpl=None): 589 super(mappingdict, self).__init__(name, tmpl) 590 self._mapping = mapping 591 592 def tomap(self, context): 593 return self._mapping 594 595 def tobool(self, context, mapping): 596 # no idea when a template mapping should be considered an empty, but 597 # a mapping dict should have at least one item in practice, so always 598 # mark this as non-empty. 599 return True 600 601 def tovalue(self, context, mapping): 602 return super(mappingdict, self).tovalue(context, mapping)[0] 603 604 605class mappingnone(wrappedvalue): 606 """Wrapper for None, but supports map operation 607 608 This represents None of Optional[mappable]. It's similar to 609 mapplinglist([]), but the underlying value is not [], but None. 610 """ 611 612 def __init__(self): 613 super(mappingnone, self).__init__(None) 614 615 def itermaps(self, context): 616 return iter([]) 617 618 619class mappedgenerator(wrapped): 620 """Wrapper for generator of strings which acts as a list 621 622 The function ``make(context, *args)`` should return a generator of 623 byte strings, or a generator of (possibly nested) generators of byte 624 strings (i.e. a generator for a list of byte strings.) 625 """ 626 627 def __init__(self, make, args=()): 628 self._make = make 629 self._args = args 630 631 def contains(self, context, mapping, item): 632 item = stringify(context, mapping, item) 633 return item in self.tovalue(context, mapping) 634 635 def _gen(self, context): 636 return self._make(context, *self._args) 637 638 def getmember(self, context, mapping, key): 639 raise error.ParseError(_(b'not a dictionary')) 640 641 def getmin(self, context, mapping): 642 return self._getby(context, mapping, min) 643 644 def getmax(self, context, mapping): 645 return self._getby(context, mapping, max) 646 647 def _getby(self, context, mapping, func): 648 xs = self.tovalue(context, mapping) 649 if not xs: 650 raise error.ParseError(_(b'empty sequence')) 651 return func(xs) 652 653 @staticmethod 654 def _filteredgen(context, mapping, make, args, select): 655 for x in make(context, *args): 656 s = stringify(context, mapping, x) 657 if select(wrappedbytes(s)): 658 yield s 659 660 def filter(self, context, mapping, select): 661 args = (mapping, self._make, self._args, select) 662 return mappedgenerator(self._filteredgen, args) 663 664 def itermaps(self, context): 665 raise error.ParseError(_(b'list of strings is not mappable')) 666 667 def join(self, context, mapping, sep): 668 return joinitems(self._gen(context), sep) 669 670 def show(self, context, mapping): 671 return self.join(context, mapping, b'') 672 673 def tobool(self, context, mapping): 674 return _nonempty(self._gen(context)) 675 676 def tovalue(self, context, mapping): 677 return [stringify(context, mapping, x) for x in self._gen(context)] 678 679 680def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None): 681 """Wrap data to support both dict-like and string-like operations""" 682 prefmt = pycompat.identity 683 if fmt is None: 684 fmt = b'%s=%s' 685 prefmt = pycompat.bytestr 686 return hybrid( 687 gen, 688 data, 689 lambda k: {key: k, value: data[k]}, 690 lambda k: fmt % (prefmt(k), prefmt(data[k])), 691 ) 692 693 694def hybridlist(data, name, fmt=None, gen=None): 695 """Wrap data to support both list-like and string-like operations""" 696 prefmt = pycompat.identity 697 if fmt is None: 698 fmt = b'%s' 699 prefmt = pycompat.bytestr 700 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x)) 701 702 703def compatdict( 704 context, 705 mapping, 706 name, 707 data, 708 key=b'key', 709 value=b'value', 710 fmt=None, 711 plural=None, 712 separator=b' ', 713): 714 """Wrap data like hybriddict(), but also supports old-style list template 715 716 This exists for backward compatibility with the old-style template. Use 717 hybriddict() for new template keywords. 718 """ 719 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)] 720 f = _showcompatlist(context, mapping, name, c, plural, separator) 721 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f) 722 723 724def compatlist( 725 context, 726 mapping, 727 name, 728 data, 729 element=None, 730 fmt=None, 731 plural=None, 732 separator=b' ', 733): 734 """Wrap data like hybridlist(), but also supports old-style list template 735 736 This exists for backward compatibility with the old-style template. Use 737 hybridlist() for new template keywords. 738 """ 739 f = _showcompatlist(context, mapping, name, data, plural, separator) 740 return hybridlist(data, name=element or name, fmt=fmt, gen=f) 741 742 743def compatfilecopiesdict(context, mapping, name, copies): 744 """Wrap list of (dest, source) file names to support old-style list 745 template and field names 746 747 This exists for backward compatibility. Use hybriddict for new template 748 keywords. 749 """ 750 # no need to provide {path} to old-style list template 751 c = [{b'name': k, b'source': v} for k, v in copies] 752 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies') 753 copies = util.sortdict(copies) 754 return hybrid( 755 f, 756 copies, 757 lambda k: {b'name': k, b'path': k, b'source': copies[k]}, 758 lambda k: b'%s (%s)' % (k, copies[k]), 759 ) 760 761 762def compatfileslist(context, mapping, name, files): 763 """Wrap list of file names to support old-style list template and field 764 names 765 766 This exists for backward compatibility. Use hybridlist for new template 767 keywords. 768 """ 769 f = _showcompatlist(context, mapping, name, files) 770 return hybrid( 771 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity 772 ) 773 774 775def _showcompatlist( 776 context, mapping, name, values, plural=None, separator=b' ' 777): 778 """Return a generator that renders old-style list template 779 780 name is name of key in template map. 781 values is list of strings or dicts. 782 plural is plural of name, if not simply name + 's'. 783 separator is used to join values as a string 784 785 expansion works like this, given name 'foo'. 786 787 if values is empty, expand 'no_foos'. 788 789 if 'foo' not in template map, return values as a string, 790 joined by 'separator'. 791 792 expand 'start_foos'. 793 794 for each value, expand 'foo'. if 'last_foo' in template 795 map, expand it instead of 'foo' for last key. 796 797 expand 'end_foos'. 798 """ 799 if not plural: 800 plural = name + b's' 801 if not values: 802 noname = b'no_' + plural 803 if context.preload(noname): 804 yield context.process(noname, mapping) 805 return 806 if not context.preload(name): 807 if isinstance(values[0], bytes): 808 yield separator.join(values) 809 else: 810 for v in values: 811 r = dict(v) 812 r.update(mapping) 813 yield r 814 return 815 startname = b'start_' + plural 816 if context.preload(startname): 817 yield context.process(startname, mapping) 818 819 def one(v, tag=name): 820 vmapping = {} 821 try: 822 vmapping.update(v) 823 # Python 2 raises ValueError if the type of v is wrong. Python 824 # 3 raises TypeError. 825 except (AttributeError, TypeError, ValueError): 826 try: 827 # Python 2 raises ValueError trying to destructure an e.g. 828 # bytes. Python 3 raises TypeError. 829 for a, b in v: 830 vmapping[a] = b 831 except (TypeError, ValueError): 832 vmapping[name] = v 833 vmapping = context.overlaymap(mapping, vmapping) 834 return context.process(tag, vmapping) 835 836 lastname = b'last_' + name 837 if context.preload(lastname): 838 last = values.pop() 839 else: 840 last = None 841 for v in values: 842 yield one(v) 843 if last is not None: 844 yield one(last, tag=lastname) 845 endname = b'end_' + plural 846 if context.preload(endname): 847 yield context.process(endname, mapping) 848 849 850def flatten(context, mapping, thing): 851 """Yield a single stream from a possibly nested set of iterators""" 852 if isinstance(thing, wrapped): 853 thing = thing.show(context, mapping) 854 if isinstance(thing, bytes): 855 yield thing 856 elif isinstance(thing, str): 857 # We can only hit this on Python 3, and it's here to guard 858 # against infinite recursion. 859 raise error.ProgrammingError( 860 b'Mercurial IO including templates is done' 861 b' with bytes, not strings, got %r' % thing 862 ) 863 elif thing is None: 864 pass 865 elif not util.safehasattr(thing, b'__iter__'): 866 yield pycompat.bytestr(thing) 867 else: 868 for i in thing: 869 if isinstance(i, wrapped): 870 i = i.show(context, mapping) 871 if isinstance(i, bytes): 872 yield i 873 elif i is None: 874 pass 875 elif not util.safehasattr(i, b'__iter__'): 876 yield pycompat.bytestr(i) 877 else: 878 for j in flatten(context, mapping, i): 879 yield j 880 881 882def stringify(context, mapping, thing): 883 """Turn values into bytes by converting into text and concatenating them""" 884 if isinstance(thing, bytes): 885 return thing # retain localstr to be round-tripped 886 return b''.join(flatten(context, mapping, thing)) 887 888 889def findsymbolicname(arg): 890 """Find symbolic name for the given compiled expression; returns None 891 if nothing found reliably""" 892 while True: 893 func, data = arg 894 if func is runsymbol: 895 return data 896 elif func is runfilter: 897 arg = data[0] 898 else: 899 return None 900 901 902def _nonempty(xiter): 903 try: 904 next(xiter) 905 return True 906 except StopIteration: 907 return False 908 909 910def _unthunk(context, mapping, thing): 911 """Evaluate a lazy byte string into value""" 912 if not isinstance(thing, types.GeneratorType): 913 return thing 914 return stringify(context, mapping, thing) 915 916 917def evalrawexp(context, mapping, arg): 918 """Evaluate given argument as a bare template object which may require 919 further processing (such as folding generator of strings)""" 920 func, data = arg 921 return func(context, mapping, data) 922 923 924def evalwrapped(context, mapping, arg): 925 """Evaluate given argument to wrapped object""" 926 thing = evalrawexp(context, mapping, arg) 927 return makewrapped(context, mapping, thing) 928 929 930def makewrapped(context, mapping, thing): 931 """Lift object to a wrapped type""" 932 if isinstance(thing, wrapped): 933 return thing 934 thing = _unthunk(context, mapping, thing) 935 if isinstance(thing, bytes): 936 return wrappedbytes(thing) 937 return wrappedvalue(thing) 938 939 940def evalfuncarg(context, mapping, arg): 941 """Evaluate given argument as value type""" 942 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg)) 943 944 945def unwrapvalue(context, mapping, thing): 946 """Move the inner value object out of the wrapper""" 947 if isinstance(thing, wrapped): 948 return thing.tovalue(context, mapping) 949 # evalrawexp() may return string, generator of strings or arbitrary object 950 # such as date tuple, but filter does not want generator. 951 return _unthunk(context, mapping, thing) 952 953 954def evalboolean(context, mapping, arg): 955 """Evaluate given argument as boolean, but also takes boolean literals""" 956 func, data = arg 957 if func is runsymbol: 958 thing = func(context, mapping, data, default=None) 959 if thing is None: 960 # not a template keyword, takes as a boolean literal 961 thing = stringutil.parsebool(data) 962 else: 963 thing = func(context, mapping, data) 964 return makewrapped(context, mapping, thing).tobool(context, mapping) 965 966 967def evaldate(context, mapping, arg, err=None): 968 """Evaluate given argument as a date tuple or a date string; returns 969 a (unixtime, offset) tuple""" 970 thing = evalrawexp(context, mapping, arg) 971 return unwrapdate(context, mapping, thing, err) 972 973 974def unwrapdate(context, mapping, thing, err=None): 975 if isinstance(thing, date): 976 return thing.tovalue(context, mapping) 977 # TODO: update hgweb to not return bare tuple; then just stringify 'thing' 978 thing = unwrapvalue(context, mapping, thing) 979 try: 980 return dateutil.parsedate(thing) 981 except AttributeError: 982 raise error.ParseError(err or _(b'not a date tuple nor a string')) 983 except error.ParseError: 984 if not err: 985 raise 986 raise error.ParseError(err) 987 988 989def evalinteger(context, mapping, arg, err=None): 990 thing = evalrawexp(context, mapping, arg) 991 return unwrapinteger(context, mapping, thing, err) 992 993 994def unwrapinteger(context, mapping, thing, err=None): 995 thing = unwrapvalue(context, mapping, thing) 996 try: 997 return int(thing) 998 except (TypeError, ValueError): 999 raise error.ParseError(err or _(b'not an integer')) 1000 1001 1002def evalstring(context, mapping, arg): 1003 return stringify(context, mapping, evalrawexp(context, mapping, arg)) 1004 1005 1006def evalstringliteral(context, mapping, arg): 1007 """Evaluate given argument as string template, but returns symbol name 1008 if it is unknown""" 1009 func, data = arg 1010 if func is runsymbol: 1011 thing = func(context, mapping, data, default=data) 1012 else: 1013 thing = func(context, mapping, data) 1014 return stringify(context, mapping, thing) 1015 1016 1017_unwrapfuncbytype = { 1018 None: unwrapvalue, 1019 bytes: stringify, 1020 date: unwrapdate, 1021 int: unwrapinteger, 1022} 1023 1024 1025def unwrapastype(context, mapping, thing, typ): 1026 """Move the inner value object out of the wrapper and coerce its type""" 1027 try: 1028 f = _unwrapfuncbytype[typ] 1029 except KeyError: 1030 raise error.ProgrammingError(b'invalid type specified: %r' % typ) 1031 return f(context, mapping, thing) 1032 1033 1034def runinteger(context, mapping, data): 1035 return int(data) 1036 1037 1038def runstring(context, mapping, data): 1039 return data 1040 1041 1042def _recursivesymbolblocker(key): 1043 def showrecursion(context, mapping): 1044 raise error.Abort(_(b"recursive reference '%s' in template") % key) 1045 1046 return showrecursion 1047 1048 1049def runsymbol(context, mapping, key, default=b''): 1050 v = context.symbol(mapping, key) 1051 if v is None: 1052 # put poison to cut recursion. we can't move this to parsing phase 1053 # because "x = {x}" is allowed if "x" is a keyword. (issue4758) 1054 safemapping = mapping.copy() 1055 safemapping[key] = _recursivesymbolblocker(key) 1056 try: 1057 v = context.process(key, safemapping) 1058 except TemplateNotFound: 1059 v = default 1060 if callable(v): 1061 # new templatekw 1062 try: 1063 return v(context, mapping) 1064 except ResourceUnavailable: 1065 # unsupported keyword is mapped to empty just like unknown keyword 1066 return None 1067 return v 1068 1069 1070def runtemplate(context, mapping, template): 1071 for arg in template: 1072 yield evalrawexp(context, mapping, arg) 1073 1074 1075def runfilter(context, mapping, data): 1076 arg, filt = data 1077 thing = evalrawexp(context, mapping, arg) 1078 intype = getattr(filt, '_intype', None) 1079 try: 1080 thing = unwrapastype(context, mapping, thing, intype) 1081 return filt(thing) 1082 except error.ParseError as e: 1083 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt)) 1084 1085 1086def _formatfiltererror(arg, filt): 1087 fn = pycompat.sysbytes(filt.__name__) 1088 sym = findsymbolicname(arg) 1089 if not sym: 1090 return _(b"incompatible use of template filter '%s'") % fn 1091 return _(b"template filter '%s' is not compatible with keyword '%s'") % ( 1092 fn, 1093 sym, 1094 ) 1095 1096 1097def _iteroverlaymaps(context, origmapping, newmappings): 1098 """Generate combined mappings from the original mapping and an iterable 1099 of partial mappings to override the original""" 1100 for i, nm in enumerate(newmappings): 1101 lm = context.overlaymap(origmapping, nm) 1102 lm[b'index'] = i 1103 yield lm 1104 1105 1106def _applymap(context, mapping, d, darg, targ): 1107 try: 1108 diter = d.itermaps(context) 1109 except error.ParseError as err: 1110 sym = findsymbolicname(darg) 1111 if not sym: 1112 raise 1113 hint = _(b"keyword '%s' does not support map operation") % sym 1114 raise error.ParseError(bytes(err), hint=hint) 1115 for lm in _iteroverlaymaps(context, mapping, diter): 1116 yield evalrawexp(context, lm, targ) 1117 1118 1119def runmap(context, mapping, data): 1120 darg, targ = data 1121 d = evalwrapped(context, mapping, darg) 1122 return mappedgenerator(_applymap, args=(mapping, d, darg, targ)) 1123 1124 1125def runmember(context, mapping, data): 1126 darg, memb = data 1127 d = evalwrapped(context, mapping, darg) 1128 if isinstance(d, mappable): 1129 lm = context.overlaymap(mapping, d.tomap(context)) 1130 return runsymbol(context, lm, memb) 1131 try: 1132 return d.getmember(context, mapping, memb) 1133 except error.ParseError as err: 1134 sym = findsymbolicname(darg) 1135 if not sym: 1136 raise 1137 hint = _(b"keyword '%s' does not support member operation") % sym 1138 raise error.ParseError(bytes(err), hint=hint) 1139 1140 1141def runnegate(context, mapping, data): 1142 data = evalinteger( 1143 context, mapping, data, _(b'negation needs an integer argument') 1144 ) 1145 return -data 1146 1147 1148def runarithmetic(context, mapping, data): 1149 func, left, right = data 1150 left = evalinteger( 1151 context, mapping, left, _(b'arithmetic only defined on integers') 1152 ) 1153 right = evalinteger( 1154 context, mapping, right, _(b'arithmetic only defined on integers') 1155 ) 1156 try: 1157 return func(left, right) 1158 except ZeroDivisionError: 1159 raise error.Abort(_(b'division by zero is not defined')) 1160 1161 1162def joinitems(itemiter, sep): 1163 """Join items with the separator; Returns generator of bytes""" 1164 first = True 1165 for x in itemiter: 1166 if first: 1167 first = False 1168 elif sep: 1169 yield sep 1170 yield x 1171