# templateutil.py - utility for template evaluation # # Copyright 2005, 2006 Olivia Mackall # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import annotations import abc import types from .i18n import _ from . import ( error, pycompat, smartset, util, ) from .utils import ( dateutil, stringutil, ) class ResourceUnavailable(error.Abort): pass class TemplateNotFound(error.Abort): pass class wrapped: # pytype: disable=ignored-metaclass """Object requiring extra conversion prior to displaying or processing as value Use unwrapvalue() or unwrapastype() to obtain the inner object. """ __metaclass__ = abc.ABCMeta @abc.abstractmethod def contains(self, context, mapping, item): """Test if the specified item is in self The item argument may be a wrapped object. """ @abc.abstractmethod def getmember(self, context, mapping, key): """Return a member item for the specified key The key argument may be a wrapped object. A returned object may be either a wrapped object or a pure value depending on the self type. """ @abc.abstractmethod def getmin(self, context, mapping): """Return the smallest item, which may be either a wrapped or a pure value depending on the self type""" @abc.abstractmethod def getmax(self, context, mapping): """Return the largest item, which may be either a wrapped or a pure value depending on the self type""" @abc.abstractmethod def filter(self, context, mapping, select): """Return new container of the same type which includes only the selected elements select() takes each item as a wrapped object and returns True/False. """ @abc.abstractmethod def itermaps(self, context): """Yield each template mapping""" @abc.abstractmethod def join(self, context, mapping, sep): """Join items with the separator; Returns a bytes or (possibly nested) generator of bytes A pre-configured template may be rendered per item if this container holds unprintable items. """ @abc.abstractmethod def show(self, context, mapping): """Return a bytes or (possibly nested) generator of bytes representing the underlying object A pre-configured template may be rendered if the underlying object is not printable. """ @abc.abstractmethod def tobool(self, context, mapping): """Return a boolean representation of the inner value""" @abc.abstractmethod def tovalue(self, context, mapping): """Move the inner value object out or create a value representation A returned value must be serializable by templaterfilters.json(). """ class mappable: # pytype: disable=ignored-metaclass """Object which can be converted to a single template mapping""" __metaclass__ = abc.ABCMeta def itermaps(self, context): yield self.tomap(context) @abc.abstractmethod def tomap(self, context): """Create a single template mapping representing this""" class wrappedbytes(wrapped): """Wrapper for byte string""" def __init__(self, value): self._value = value def contains(self, context, mapping, item): item = stringify(context, mapping, item) return item in self._value def getmember(self, context, mapping, key): raise error.ParseError( _(b'%r is not a dictionary') % pycompat.bytestr(self._value) ) def getmin(self, context, mapping): return self._getby(context, mapping, min) def getmax(self, context, mapping): return self._getby(context, mapping, max) def _getby(self, context, mapping, func): if not self._value: raise error.ParseError(_(b'empty string')) return func(pycompat.iterbytestr(self._value)) def filter(self, context, mapping, select): raise error.ParseError( _(b'%r is not filterable') % pycompat.bytestr(self._value) ) def itermaps(self, context): raise error.ParseError( _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value) ) def join(self, context, mapping, sep): return joinitems(pycompat.iterbytestr(self._value), sep) def show(self, context, mapping): return self._value def tobool(self, context, mapping): return bool(self._value) def tovalue(self, context, mapping): return self._value class wrappedvalue(wrapped): """Generic wrapper for pure non-list/dict/bytes value""" def __init__(self, value): self._value = value def contains(self, context, mapping, item): raise error.ParseError(_(b"%r is not iterable") % self._value) def getmember(self, context, mapping, key): raise error.ParseError(_(b'%r is not a dictionary') % self._value) def getmin(self, context, mapping): raise error.ParseError(_(b"%r is not iterable") % self._value) def getmax(self, context, mapping): raise error.ParseError(_(b"%r is not iterable") % self._value) def filter(self, context, mapping, select): raise error.ParseError(_(b"%r is not iterable") % self._value) def itermaps(self, context): raise error.ParseError( _(b'%r is not iterable of mappings') % self._value ) def join(self, context, mapping, sep): raise error.ParseError(_(b'%r is not iterable') % self._value) def show(self, context, mapping): if self._value is None: return b'' return pycompat.bytestr(self._value) def tobool(self, context, mapping): if self._value is None: return False if isinstance(self._value, bool): return self._value # otherwise evaluate as string, which means 0 is True return bool(pycompat.bytestr(self._value)) def tovalue(self, context, mapping): return self._value class date(mappable, wrapped): """Wrapper for date tuple""" def __init__(self, value, showfmt=b'%d %d'): # value may be (float, int), but public interface shouldn't support # floating-point timestamp self._unixtime, self._tzoffset = map(int, value) self._showfmt = showfmt def contains(self, context, mapping, item): raise error.ParseError(_(b'date is not iterable')) def getmember(self, context, mapping, key): raise error.ParseError(_(b'date is not a dictionary')) def getmin(self, context, mapping): raise error.ParseError(_(b'date is not iterable')) def getmax(self, context, mapping): raise error.ParseError(_(b'date is not iterable')) def filter(self, context, mapping, select): raise error.ParseError(_(b'date is not iterable')) def join(self, context, mapping, sep): raise error.ParseError(_(b"date is not iterable")) def show(self, context, mapping): return self._showfmt % (self._unixtime, self._tzoffset) def tomap(self, context): return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset} def tobool(self, context, mapping): return True def tovalue(self, context, mapping): return (self._unixtime, self._tzoffset) class hybrid(wrapped): """Wrapper for list or dict to support legacy template This class allows us to handle both: - "{files}" (legacy command-line-specific list hack) and - "{files % '{file}\n'}" (hgweb-style with inlining and function support) and to access raw values: - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}" - "{get(extras, key)}" - "{files|json}" """ def __init__(self, gen, values, makemap, joinfmt, keytype=None): self._gen = gen # generator or function returning generator self._values = values self._makemap = makemap self._joinfmt = joinfmt self._keytype = keytype # hint for 'x in y' where type(x) is unresolved def contains(self, context, mapping, item): item = unwrapastype(context, mapping, item, self._keytype) return item in self._values def getmember(self, context, mapping, key): # TODO: maybe split hybrid list/dict types? if not hasattr(self._values, 'get'): raise error.ParseError(_(b'not a dictionary')) key = unwrapastype(context, mapping, key, self._keytype) return self._wrapvalue(key, self._values.get(key)) def getmin(self, context, mapping): return self._getby(context, mapping, min) def getmax(self, context, mapping): return self._getby(context, mapping, max) def _getby(self, context, mapping, func): if not self._values: raise error.ParseError(_(b'empty sequence')) val = func(self._values) return self._wrapvalue(val, val) def _wrapvalue(self, key, val): if val is None: return if hasattr(val, '_makemap'): # a nested hybrid list/dict, which has its own way of map operation return val return hybriditem(None, key, val, self._makemap) def filter(self, context, mapping, select): if hasattr(self._values, 'get'): values = { k: v for k, v in self._values.items() if select(self._wrapvalue(k, v)) } else: values = [v for v in self._values if select(self._wrapvalue(v, v))] return hybrid(None, values, self._makemap, self._joinfmt, self._keytype) def itermaps(self, context): makemap = self._makemap for x in self._values: yield makemap(x) def join(self, context, mapping, sep): # TODO: switch gen to (context, mapping) API? return joinitems((self._joinfmt(x) for x in self._values), sep) def show(self, context, mapping): # TODO: switch gen to (context, mapping) API? gen = self._gen if gen is None: return self.join(context, mapping, b' ') if callable(gen): return gen() return gen def tobool(self, context, mapping): return bool(self._values) def tovalue(self, context, mapping): # TODO: make it non-recursive for trivial lists/dicts xs = self._values if hasattr(xs, 'get'): return {k: unwrapvalue(context, mapping, v) for k, v in xs.items()} return [unwrapvalue(context, mapping, x) for x in xs] class hybriditem(mappable, wrapped): """Wrapper for non-list/dict object to support map operation This class allows us to handle both: - "{manifest}" - "{manifest % '{rev}:{node}'}" - "{manifest.rev}" """ def __init__(self, gen, key, value, makemap): self._gen = gen # generator or function returning generator self._key = key self._value = value # may be generator of strings self._makemap = makemap def tomap(self, context): return self._makemap(self._key) def contains(self, context, mapping, item): w = makewrapped(context, mapping, self._value) return w.contains(context, mapping, item) def getmember(self, context, mapping, key): w = makewrapped(context, mapping, self._value) return w.getmember(context, mapping, key) def getmin(self, context, mapping): w = makewrapped(context, mapping, self._value) return w.getmin(context, mapping) def getmax(self, context, mapping): w = makewrapped(context, mapping, self._value) return w.getmax(context, mapping) def filter(self, context, mapping, select): w = makewrapped(context, mapping, self._value) return w.filter(context, mapping, select) def join(self, context, mapping, sep): w = makewrapped(context, mapping, self._value) return w.join(context, mapping, sep) def show(self, context, mapping): # TODO: switch gen to (context, mapping) API? gen = self._gen if gen is None: return pycompat.bytestr(self._value) if callable(gen): return gen() return gen def tobool(self, context, mapping): w = makewrapped(context, mapping, self._value) return w.tobool(context, mapping) def tovalue(self, context, mapping): return _unthunk(context, mapping, self._value) class revslist(wrapped): """Wrapper for a smartset (a list/set of revision numbers) If name specified, the revs will be rendered with the old-style list template of the given name by default. The cachekey provides a hint to cache further computation on this smartset. If the underlying smartset is dynamically created, the cachekey should be None. """ def __init__(self, repo, revs, name=None, cachekey=None): assert isinstance(revs, smartset.abstractsmartset) self._repo = repo self._revs = revs self._name = name self.cachekey = cachekey def contains(self, context, mapping, item): rev = unwrapinteger(context, mapping, item) return rev in self._revs def getmember(self, context, mapping, key): raise error.ParseError(_(b'not a dictionary')) def getmin(self, context, mapping): makehybriditem = self._makehybriditemfunc() return makehybriditem(self._revs.min()) def getmax(self, context, mapping): makehybriditem = self._makehybriditemfunc() return makehybriditem(self._revs.max()) def filter(self, context, mapping, select): makehybriditem = self._makehybriditemfunc() frevs = self._revs.filter(lambda r: select(makehybriditem(r))) # once filtered, no need to support old-style list template return revslist(self._repo, frevs, name=None) def itermaps(self, context): makemap = self._makemapfunc() for r in self._revs: yield makemap(r) def _makehybriditemfunc(self): makemap = self._makemapfunc() return lambda r: hybriditem(None, r, r, makemap) def _makemapfunc(self): repo = self._repo name = self._name if name: return lambda r: {name: r, b'ctx': repo[r]} else: return lambda r: {b'ctx': repo[r]} def join(self, context, mapping, sep): return joinitems(self._revs, sep) def show(self, context, mapping): if self._name: srevs = [b'%d' % r for r in self._revs] return _showcompatlist(context, mapping, self._name, srevs) else: return self.join(context, mapping, b' ') def tobool(self, context, mapping): return bool(self._revs) def tovalue(self, context, mapping): return self._revs class _mappingsequence(wrapped): """Wrapper for sequence of template mappings This represents an inner template structure (i.e. a list of dicts), which can also be rendered by the specified named/literal template. Template mappings may be nested. """ def __init__(self, name=None, tmpl=None, sep=b''): if name is not None and tmpl is not None: raise error.ProgrammingError( b'name and tmpl are mutually exclusive' ) self._name = name self._tmpl = tmpl self._defaultsep = sep def contains(self, context, mapping, item): raise error.ParseError(_(b'not comparable')) def getmember(self, context, mapping, key): raise error.ParseError(_(b'not a dictionary')) def getmin(self, context, mapping): raise error.ParseError(_(b'not comparable')) def getmax(self, context, mapping): raise error.ParseError(_(b'not comparable')) def filter(self, context, mapping, select): # implement if necessary; we'll need a wrapped type for a mapping dict raise error.ParseError(_(b'not filterable without template')) def join(self, context, mapping, sep): mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context)) if self._name: itemiter = (context.process(self._name, m) for m in mapsiter) elif self._tmpl: itemiter = (context.expand(self._tmpl, m) for m in mapsiter) else: raise error.ParseError(_(b'not displayable without template')) return joinitems(itemiter, sep) def show(self, context, mapping): return self.join(context, mapping, self._defaultsep) def tovalue(self, context, mapping): knownres = context.knownresourcekeys() items = [] for nm in self.itermaps(context): # drop internal resources (recursively) which shouldn't be displayed lm = context.overlaymap(mapping, nm) items.append( { k: unwrapvalue(context, lm, v) for k, v in nm.items() if k not in knownres } ) return items class mappinggenerator(_mappingsequence): """Wrapper for generator of template mappings The function ``make(context, *args)`` should return a generator of mapping dicts. """ def __init__(self, make, args=(), name=None, tmpl=None, sep=b''): super(mappinggenerator, self).__init__(name, tmpl, sep) self._make = make self._args = args def itermaps(self, context): return self._make(context, *self._args) def tobool(self, context, mapping): return _nonempty(self.itermaps(context)) class mappinglist(_mappingsequence): """Wrapper for list of template mappings""" def __init__(self, mappings, name=None, tmpl=None, sep=b''): super(mappinglist, self).__init__(name, tmpl, sep) self._mappings = mappings def itermaps(self, context): return iter(self._mappings) def tobool(self, context, mapping): return bool(self._mappings) class mappingdict(mappable, _mappingsequence): """Wrapper for a single template mapping This isn't a sequence in a way that the underlying dict won't be iterated as a dict, but shares most of the _mappingsequence functions. """ def __init__(self, mapping, name=None, tmpl=None): super(mappingdict, self).__init__(name, tmpl) self._mapping = mapping def tomap(self, context): return self._mapping def tobool(self, context, mapping): # no idea when a template mapping should be considered an empty, but # a mapping dict should have at least one item in practice, so always # mark this as non-empty. return True def tovalue(self, context, mapping): return super(mappingdict, self).tovalue(context, mapping)[0] class mappingnone(wrappedvalue): """Wrapper for None, but supports map operation This represents None of Optional[mappable]. It's similar to mapplinglist([]), but the underlying value is not [], but None. """ def __init__(self): super(mappingnone, self).__init__(None) def itermaps(self, context): return iter([]) class mappedgenerator(wrapped): """Wrapper for generator of strings which acts as a list The function ``make(context, *args)`` should return a generator of byte strings, or a generator of (possibly nested) generators of byte strings (i.e. a generator for a list of byte strings.) """ def __init__(self, make, args=()): self._make = make self._args = args def contains(self, context, mapping, item): item = stringify(context, mapping, item) return item in self.tovalue(context, mapping) def _gen(self, context): return self._make(context, *self._args) def getmember(self, context, mapping, key): raise error.ParseError(_(b'not a dictionary')) def getmin(self, context, mapping): return self._getby(context, mapping, min) def getmax(self, context, mapping): return self._getby(context, mapping, max) def _getby(self, context, mapping, func): xs = self.tovalue(context, mapping) if not xs: raise error.ParseError(_(b'empty sequence')) return func(xs) @staticmethod def _filteredgen(context, mapping, make, args, select): for x in make(context, *args): s = stringify(context, mapping, x) if select(wrappedbytes(s)): yield s def filter(self, context, mapping, select): args = (mapping, self._make, self._args, select) return mappedgenerator(self._filteredgen, args) def itermaps(self, context): raise error.ParseError(_(b'list of strings is not mappable')) def join(self, context, mapping, sep): return joinitems(self._gen(context), sep) def show(self, context, mapping): return self.join(context, mapping, b'') def tobool(self, context, mapping): return _nonempty(self._gen(context)) def tovalue(self, context, mapping): return [stringify(context, mapping, x) for x in self._gen(context)] def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None): """Wrap data to support both dict-like and string-like operations""" prefmt = pycompat.identity if fmt is None: fmt = b'%s=%s' prefmt = pycompat.bytestr return hybrid( gen, data, lambda k: {key: k, value: data[k]}, lambda k: fmt % (prefmt(k), prefmt(data[k])), ) def hybridlist(data, name, fmt=None, gen=None): """Wrap data to support both list-like and string-like operations""" prefmt = pycompat.identity if fmt is None: fmt = b'%s' prefmt = pycompat.bytestr return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x)) def compatdict( context, mapping, name, data, key=b'key', value=b'value', fmt=None, plural=None, separator=b' ', ): """Wrap data like hybriddict(), but also supports old-style list template This exists for backward compatibility with the old-style template. Use hybriddict() for new template keywords. """ c = [{key: k, value: v} for k, v in data.items()] f = _showcompatlist(context, mapping, name, c, plural, separator) return hybriddict(data, key=key, value=value, fmt=fmt, gen=f) def compatlist( context, mapping, name, data, element=None, fmt=None, plural=None, separator=b' ', ): """Wrap data like hybridlist(), but also supports old-style list template This exists for backward compatibility with the old-style template. Use hybridlist() for new template keywords. """ f = _showcompatlist(context, mapping, name, data, plural, separator) return hybridlist(data, name=element or name, fmt=fmt, gen=f) def compatfilecopiesdict(context, mapping, name, copies): """Wrap list of (dest, source) file names to support old-style list template and field names This exists for backward compatibility. Use hybriddict for new template keywords. """ # no need to provide {path} to old-style list template c = [{b'name': k, b'source': v} for k, v in copies] f = _showcompatlist(context, mapping, name, c, plural=b'file_copies') copies = util.sortdict(copies) return hybrid( f, copies, lambda k: {b'name': k, b'path': k, b'source': copies[k]}, lambda k: b'%s (%s)' % (k, copies[k]), ) def compatfileslist(context, mapping, name, files): """Wrap list of file names to support old-style list template and field names This exists for backward compatibility. Use hybridlist for new template keywords. """ f = _showcompatlist(context, mapping, name, files) return hybrid( f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity ) def _showcompatlist( context, mapping, name, values, plural=None, separator=b' ' ): """Return a generator that renders old-style list template name is name of key in template map. values is list of strings or dicts. plural is plural of name, if not simply name + 's'. separator is used to join values as a string expansion works like this, given name 'foo'. if values is empty, expand 'no_foos'. if 'foo' not in template map, return values as a string, joined by 'separator'. expand 'start_foos'. for each value, expand 'foo'. if 'last_foo' in template map, expand it instead of 'foo' for last key. expand 'end_foos'. """ if not plural: plural = name + b's' if not values: noname = b'no_' + plural if context.preload(noname): yield context.process(noname, mapping) return if not context.preload(name): if isinstance(values[0], bytes): yield separator.join(values) else: for v in values: r = dict(v) r.update(mapping) yield r return startname = b'start_' + plural if context.preload(startname): yield context.process(startname, mapping) def one(v, tag=name): vmapping = {} try: vmapping.update(v) # Python 2 raises ValueError if the type of v is wrong. Python # 3 raises TypeError. except (AttributeError, TypeError, ValueError): try: # Python 2 raises ValueError trying to destructure an e.g. # bytes. Python 3 raises TypeError. for a, b in v: vmapping[a] = b except (TypeError, ValueError): vmapping[name] = v vmapping = context.overlaymap(mapping, vmapping) return context.process(tag, vmapping) lastname = b'last_' + name if context.preload(lastname): last = values.pop() else: last = None for v in values: yield one(v) if last is not None: yield one(last, tag=lastname) endname = b'end_' + plural if context.preload(endname): yield context.process(endname, mapping) def flatten(context, mapping, thing): """Yield a single stream from a possibly nested set of iterators""" if isinstance(thing, wrapped): thing = thing.show(context, mapping) if isinstance(thing, bytes): yield thing elif isinstance(thing, str): # We can only hit this on Python 3, and it's here to guard # against infinite recursion. raise error.ProgrammingError( b'Mercurial IO including templates is done' b' with bytes, not strings, got %r' % thing ) elif thing is None: pass elif not hasattr(thing, '__iter__'): yield pycompat.bytestr(thing) else: for i in thing: if isinstance(i, wrapped): i = i.show(context, mapping) if isinstance(i, bytes): yield i elif i is None: pass elif not hasattr(i, '__iter__'): yield pycompat.bytestr(i) else: for j in flatten(context, mapping, i): yield j def stringify(context, mapping, thing): """Turn values into bytes by converting into text and concatenating them""" if isinstance(thing, bytes): return thing # retain localstr to be round-tripped return b''.join(flatten(context, mapping, thing)) def findsymbolicname(arg): """Find symbolic name for the given compiled expression; returns None if nothing found reliably""" while True: func, data = arg if func is runsymbol: return data elif func is runfilter: arg = data[0] else: return None def _nonempty(xiter): try: next(xiter) return True except StopIteration: return False def _unthunk(context, mapping, thing): """Evaluate a lazy byte string into value""" if not isinstance(thing, types.GeneratorType): return thing return stringify(context, mapping, thing) def evalrawexp(context, mapping, arg): """Evaluate given argument as a bare template object which may require further processing (such as folding generator of strings)""" func, data = arg return func(context, mapping, data) def evalwrapped(context, mapping, arg): """Evaluate given argument to wrapped object""" thing = evalrawexp(context, mapping, arg) return makewrapped(context, mapping, thing) def makewrapped(context, mapping, thing): """Lift object to a wrapped type""" if isinstance(thing, wrapped): return thing thing = _unthunk(context, mapping, thing) if isinstance(thing, bytes): return wrappedbytes(thing) return wrappedvalue(thing) def evalfuncarg(context, mapping, arg): """Evaluate given argument as value type""" return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg)) def unwrapvalue(context, mapping, thing): """Move the inner value object out of the wrapper""" if isinstance(thing, wrapped): return thing.tovalue(context, mapping) # evalrawexp() may return string, generator of strings or arbitrary object # such as date tuple, but filter does not want generator. return _unthunk(context, mapping, thing) def evalboolean(context, mapping, arg): """Evaluate given argument as boolean, but also takes boolean literals""" func, data = arg if func is runsymbol: thing = func(context, mapping, data, default=None) if thing is None: # not a template keyword, takes as a boolean literal thing = stringutil.parsebool(data) else: thing = func(context, mapping, data) return makewrapped(context, mapping, thing).tobool(context, mapping) def evaldate(context, mapping, arg, err=None): """Evaluate given argument as a date tuple or a date string; returns a (unixtime, offset) tuple""" thing = evalrawexp(context, mapping, arg) return unwrapdate(context, mapping, thing, err) def unwrapdate(context, mapping, thing, err=None): if isinstance(thing, date): return thing.tovalue(context, mapping) # TODO: update hgweb to not return bare tuple; then just stringify 'thing' thing = unwrapvalue(context, mapping, thing) try: return dateutil.parsedate(thing) except AttributeError: raise error.ParseError(err or _(b'not a date tuple nor a string')) except error.ParseError: if not err: raise raise error.ParseError(err) def evalinteger(context, mapping, arg, err=None): thing = evalrawexp(context, mapping, arg) return unwrapinteger(context, mapping, thing, err) def unwrapinteger(context, mapping, thing, err=None): thing = unwrapvalue(context, mapping, thing) try: return int(thing) except (TypeError, ValueError): raise error.ParseError(err or _(b'not an integer')) def evalstring(context, mapping, arg): return stringify(context, mapping, evalrawexp(context, mapping, arg)) def evalstringliteral(context, mapping, arg): """Evaluate given argument as string template, but returns symbol name if it is unknown""" func, data = arg if func is runsymbol: thing = func(context, mapping, data, default=data) else: thing = func(context, mapping, data) return stringify(context, mapping, thing) _unwrapfuncbytype = { None: unwrapvalue, bytes: stringify, date: unwrapdate, int: unwrapinteger, } def unwrapastype(context, mapping, thing, typ): """Move the inner value object out of the wrapper and coerce its type""" try: f = _unwrapfuncbytype[typ] except KeyError: raise error.ProgrammingError(b'invalid type specified: %r' % typ) return f(context, mapping, thing) def runinteger(context, mapping, data): return int(data) def runstring(context, mapping, data): return data def _recursivesymbolblocker(key): def showrecursion(context, mapping): raise error.Abort(_(b"recursive reference '%s' in template") % key) return showrecursion def runsymbol(context, mapping, key, default=b''): v = context.symbol(mapping, key) if v is None: # put poison to cut recursion. we can't move this to parsing phase # because "x = {x}" is allowed if "x" is a keyword. (issue4758) safemapping = mapping.copy() safemapping[key] = _recursivesymbolblocker(key) try: v = context.process(key, safemapping) except TemplateNotFound: v = default if callable(v): # new templatekw try: return v(context, mapping) except ResourceUnavailable: # unsupported keyword is mapped to empty just like unknown keyword return None return v def runtemplate(context, mapping, template): for arg in template: yield evalrawexp(context, mapping, arg) def runfilter(context, mapping, data): arg, filt = data thing = evalrawexp(context, mapping, arg) intype = getattr(filt, '_intype', None) try: thing = unwrapastype(context, mapping, thing, intype) return filt(thing) except error.ParseError as e: raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt)) def _formatfiltererror(arg, filt): fn = pycompat.sysbytes(filt.__name__) sym = findsymbolicname(arg) if not sym: return _(b"incompatible use of template filter '%s'") % fn return _(b"template filter '%s' is not compatible with keyword '%s'") % ( fn, sym, ) def _iteroverlaymaps(context, origmapping, newmappings): """Generate combined mappings from the original mapping and an iterable of partial mappings to override the original""" for i, nm in enumerate(newmappings): lm = context.overlaymap(origmapping, nm) lm[b'index'] = i yield lm def _applymap(context, mapping, d, darg, targ): try: diter = d.itermaps(context) except error.ParseError as err: sym = findsymbolicname(darg) if not sym: raise hint = _(b"keyword '%s' does not support map operation") % sym raise error.ParseError(bytes(err), hint=hint) for lm in _iteroverlaymaps(context, mapping, diter): yield evalrawexp(context, lm, targ) def runmap(context, mapping, data): darg, targ = data d = evalwrapped(context, mapping, darg) return mappedgenerator(_applymap, args=(mapping, d, darg, targ)) def runmember(context, mapping, data): darg, memb = data d = evalwrapped(context, mapping, darg) if isinstance(d, mappable): lm = context.overlaymap(mapping, d.tomap(context)) return runsymbol(context, lm, memb) try: return d.getmember(context, mapping, memb) except error.ParseError as err: sym = findsymbolicname(darg) if not sym: raise hint = _(b"keyword '%s' does not support member operation") % sym raise error.ParseError(bytes(err), hint=hint) def runnegate(context, mapping, data): data = evalinteger( context, mapping, data, _(b'negation needs an integer argument') ) return -data def runarithmetic(context, mapping, data): func, left, right = data left = evalinteger( context, mapping, left, _(b'arithmetic only defined on integers') ) right = evalinteger( context, mapping, right, _(b'arithmetic only defined on integers') ) try: return func(left, right) except ZeroDivisionError: raise error.Abort(_(b'division by zero is not defined')) def joinitems(itemiter, sep): """Join items with the separator; Returns generator of bytes""" first = True for x in itemiter: if first: first = False elif sep: yield sep yield x