diff mercurial/templateutil.py @ 36913:da2977e674a3

templater: extract template evaluation utility to new module Prepares for splitting template functions to new module. All eval* functions were moved to templateutil.py, and run* functions had to be moved as well due to the dependency from eval*s. eval*s were aliased as they are commonly used in codebase. _getdictitem() had to be made public.
author Yuya Nishihara <yuya@tcha.org>
date Thu, 08 Mar 2018 22:33:24 +0900
parents mercurial/templater.py@543afbdc8e59
children 6ff6e1d6b5b8
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templateutil.py	Thu Mar 08 22:33:24 2018 +0900
@@ -0,0 +1,227 @@
+# templateutil.py - utility for template evaluation
+#
+# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
+#
+# 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 absolute_import
+
+import types
+
+from .i18n import _
+from . import (
+    error,
+    pycompat,
+    templatefilters,
+    templatekw,
+    util,
+)
+
+class ResourceUnavailable(error.Abort):
+    pass
+
+class TemplateNotFound(error.Abort):
+    pass
+
+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 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 evalfuncarg(context, mapping, arg):
+    """Evaluate given argument as value type"""
+    thing = evalrawexp(context, mapping, arg)
+    thing = templatekw.unwrapvalue(thing)
+    # evalrawexp() may return string, generator of strings or arbitrary object
+    # such as date tuple, but filter does not want generator.
+    if isinstance(thing, types.GeneratorType):
+        thing = stringify(thing)
+    return 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 = util.parsebool(data)
+    else:
+        thing = func(context, mapping, data)
+    thing = templatekw.unwrapvalue(thing)
+    if isinstance(thing, bool):
+        return thing
+    # other objects are evaluated as strings, which means 0 is True, but
+    # empty dict/list should be False as they are expected to be ''
+    return bool(stringify(thing))
+
+def evalinteger(context, mapping, arg, err=None):
+    v = evalfuncarg(context, mapping, arg)
+    try:
+        return int(v)
+    except (TypeError, ValueError):
+        raise error.ParseError(err or _('not an integer'))
+
+def evalstring(context, mapping, arg):
+    return stringify(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(thing)
+
+_evalfuncbytype = {
+    bool: evalboolean,
+    bytes: evalstring,
+    int: evalinteger,
+}
+
+def evalastype(context, mapping, arg, typ):
+    """Evaluate given argument and coerce its type"""
+    try:
+        f = _evalfuncbytype[typ]
+    except KeyError:
+        raise error.ProgrammingError('invalid type specified: %r' % typ)
+    return f(context, mapping, arg)
+
+def runinteger(context, mapping, data):
+    return int(data)
+
+def runstring(context, mapping, data):
+    return data
+
+def _recursivesymbolblocker(key):
+    def showrecursion(**args):
+        raise error.Abort(_("recursive reference '%s' in template") % key)
+    return showrecursion
+
+def runsymbol(context, mapping, key, default=''):
+    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) and getattr(v, '_requires', None) is None:
+        # old templatekw: expand all keywords and resources
+        props = context._resources.copy()
+        props.update(mapping)
+        return v(**pycompat.strkwargs(props))
+    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 = evalfuncarg(context, mapping, arg)
+    try:
+        return filt(thing)
+    except (ValueError, AttributeError, TypeError):
+        sym = findsymbolicname(arg)
+        if sym:
+            msg = (_("template filter '%s' is not compatible with keyword '%s'")
+                   % (pycompat.sysbytes(filt.__name__), sym))
+        else:
+            msg = (_("incompatible use of template filter '%s'")
+                   % pycompat.sysbytes(filt.__name__))
+        raise error.Abort(msg)
+
+def runmap(context, mapping, data):
+    darg, targ = data
+    d = evalrawexp(context, mapping, darg)
+    if util.safehasattr(d, 'itermaps'):
+        diter = d.itermaps()
+    else:
+        try:
+            diter = iter(d)
+        except TypeError:
+            sym = findsymbolicname(darg)
+            if sym:
+                raise error.ParseError(_("keyword '%s' is not iterable") % sym)
+            else:
+                raise error.ParseError(_("%r is not iterable") % d)
+
+    for i, v in enumerate(diter):
+        lm = mapping.copy()
+        lm['index'] = i
+        if isinstance(v, dict):
+            lm.update(v)
+            lm['originalnode'] = mapping.get('node')
+            yield evalrawexp(context, lm, targ)
+        else:
+            # v is not an iterable of dicts, this happen when 'key'
+            # has been fully expanded already and format is useless.
+            # If so, return the expanded value.
+            yield v
+
+def runmember(context, mapping, data):
+    darg, memb = data
+    d = evalrawexp(context, mapping, darg)
+    if util.safehasattr(d, 'tomap'):
+        lm = mapping.copy()
+        lm.update(d.tomap())
+        return runsymbol(context, lm, memb)
+    if util.safehasattr(d, 'get'):
+        return getdictitem(d, memb)
+
+    sym = findsymbolicname(darg)
+    if sym:
+        raise error.ParseError(_("keyword '%s' has no member") % sym)
+    else:
+        raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
+
+def runnegate(context, mapping, data):
+    data = evalinteger(context, mapping, data,
+                       _('negation needs an integer argument'))
+    return -data
+
+def runarithmetic(context, mapping, data):
+    func, left, right = data
+    left = evalinteger(context, mapping, left,
+                       _('arithmetic only defined on integers'))
+    right = evalinteger(context, mapping, right,
+                        _('arithmetic only defined on integers'))
+    try:
+        return func(left, right)
+    except ZeroDivisionError:
+        raise error.Abort(_('division by zero is not defined'))
+
+def getdictitem(dictarg, key):
+    val = dictarg.get(key)
+    if val is None:
+        return
+    return templatekw.wraphybridvalue(dictarg, key, val)
+
+stringify = templatefilters.stringify