templater: extract template evaluation utility to new module
authorYuya Nishihara <yuya@tcha.org>
Thu, 08 Mar 2018 22:33:24 +0900
changeset 36913 da2977e674a3
parent 36912 543afbdc8e59
child 36914 d255744de97a
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.
mercurial/templater.py
mercurial/templateutil.py
--- a/mercurial/templater.py	Thu Mar 08 22:20:36 2018 +0900
+++ b/mercurial/templater.py	Thu Mar 08 22:33:24 2018 +0900
@@ -9,7 +9,6 @@
 
 import os
 import re
-import types
 
 from .i18n import _
 from . import (
@@ -27,15 +26,18 @@
     scmutil,
     templatefilters,
     templatekw,
+    templateutil,
     util,
 )
 from .utils import dateutil
 
-class ResourceUnavailable(error.Abort):
-    pass
-
-class TemplateNotFound(error.Abort):
-    pass
+evalrawexp = templateutil.evalrawexp
+evalfuncarg = templateutil.evalfuncarg
+evalboolean = templateutil.evalboolean
+evalinteger = templateutil.evalinteger
+evalstring = templateutil.evalstring
+evalstringliteral = templateutil.evalstringliteral
+evalastype = templateutil.evalastype
 
 # template parsing
 
@@ -361,237 +363,43 @@
         return context._load(exp[1])
     raise error.ParseError(_("expected template specifier"))
 
-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 _runrecursivesymbol(context, mapping, key):
     raise error.Abort(_("recursive reference '%s' in template") % key)
 
-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 buildtemplate(exp, context):
     ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
-    return (runtemplate, ctmpl)
-
-def runtemplate(context, mapping, template):
-    for arg in template:
-        yield evalrawexp(context, mapping, arg)
+    return (templateutil.runtemplate, ctmpl)
 
 def buildfilter(exp, context):
     n = getsymbol(exp[2])
     if n in context._filters:
         filt = context._filters[n]
         arg = compileexp(exp[1], context, methods)
-        return (runfilter, (arg, filt))
+        return (templateutil.runfilter, (arg, filt))
     if n in context._funcs:
         f = context._funcs[n]
         args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
         return (f, args)
     raise error.ParseError(_("unknown function '%s'") % n)
 
-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 buildmap(exp, context):
     darg = compileexp(exp[1], context, methods)
     targ = gettemplate(exp[2], context)
-    return (runmap, (darg, targ))
-
-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
+    return (templateutil.runmap, (darg, targ))
 
 def buildmember(exp, context):
     darg = compileexp(exp[1], context, methods)
     memb = getsymbol(exp[2])
-    return (runmember, (darg, memb))
-
-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))
+    return (templateutil.runmember, (darg, memb))
 
 def buildnegate(exp, context):
     arg = compileexp(exp[1], context, exprmethods)
-    return (runnegate, arg)
-
-def runnegate(context, mapping, data):
-    data = evalinteger(context, mapping, data,
-                       _('negation needs an integer argument'))
-    return -data
+    return (templateutil.runnegate, arg)
 
 def buildarithmetic(exp, context, func):
     left = compileexp(exp[1], context, exprmethods)
     right = compileexp(exp[2], context, exprmethods)
-    return (runarithmetic, (func, left, right))
-
-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'))
+    return (templateutil.runarithmetic, (func, left, right))
 
 def buildfunc(exp, context):
     n = getsymbol(exp[1])
@@ -604,7 +412,7 @@
         if len(args) != 1:
             raise error.ParseError(_("filter %s expects one argument") % n)
         f = context._filters[n]
-        return (runfilter, (args[0], f))
+        return (templateutil.runfilter, (args[0], f))
     raise error.ParseError(_("unknown function '%s'") % n)
 
 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
@@ -681,7 +489,7 @@
     data = util.sortdict()
 
     for v in args['args']:
-        k = findsymbolicname(v)
+        k = templateutil.findsymbolicname(v)
         if not k:
             raise error.ParseError(_('dict key cannot be inferred'))
         if k in data or k in args['kwargs']:
@@ -848,13 +656,7 @@
         raise error.ParseError(_("get() expects a dict as first argument"))
 
     key = evalfuncarg(context, mapping, args[1])
-    return _getdictitem(dictarg, key)
-
-def _getdictitem(dictarg, key):
-    val = dictarg.get(key)
-    if val is None:
-        return
-    return templatekw.wraphybridvalue(dictarg, key, val)
+    return templateutil.getdictitem(dictarg, key)
 
 @templatefunc('if(expr, then[, else])')
 def if_(context, mapping, args):
@@ -1031,7 +833,8 @@
         raise error.ParseError(_("mod expects two arguments"))
 
     func = lambda a, b: a % b
-    return runarithmetic(context, mapping, (func, args[0], args[1]))
+    return templateutil.runarithmetic(context, mapping,
+                                      (func, args[0], args[1]))
 
 @templatefunc('obsfateoperations(markers)')
 def obsfateoperations(context, mapping, args):
@@ -1273,9 +1076,9 @@
 
 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
 exprmethods = {
-    "integer": lambda e, c: (runinteger, e[1]),
-    "string": lambda e, c: (runstring, e[1]),
-    "symbol": lambda e, c: (runsymbol, e[1]),
+    "integer": lambda e, c: (templateutil.runinteger, e[1]),
+    "string": lambda e, c: (templateutil.runstring, e[1]),
+    "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
     "template": buildtemplate,
     "group": lambda e, c: compileexp(e[1], c, exprmethods),
     ".": buildmember,
@@ -1404,8 +1207,8 @@
         if v is None:
             v = self._resources.get(key)
         if v is None:
-            raise ResourceUnavailable(_('template resource not available: %s')
-                                      % key)
+            raise templateutil.ResourceUnavailable(
+                _('template resource not available: %s') % key)
         return v
 
     def _load(self, t):
@@ -1552,8 +1355,8 @@
             try:
                 self.cache[t] = util.readfile(self.map[t][1])
             except KeyError as inst:
-                raise TemplateNotFound(_('"%s" not in template map') %
-                                       inst.args[0])
+                raise templateutil.TemplateNotFound(
+                    _('"%s" not in template map') % inst.args[0])
             except IOError as inst:
                 reason = (_('template file %s: %s')
                           % (self.map[t][1], util.forcebytestr(inst.args[1])))
--- /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