# HG changeset patch # User Yuya Nishihara # Date 1520516004 -32400 # Node ID da2977e674a365fa3ebb57655f0683f6f9d5f546 # Parent 543afbdc8e591e7271f19e96ca08f3bd924083dc 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. diff -r 543afbdc8e59 -r da2977e674a3 mercurial/templater.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]))) diff -r 543afbdc8e59 -r da2977e674a3 mercurial/templateutil.py --- /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 +# +# 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