Mercurial > public > mercurial-scm > hg
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