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