mercurial/templater.py
changeset 36913 da2977e674a3
parent 36912 543afbdc8e59
child 36920 6ff6e1d6b5b8
equal deleted inserted replaced
36912:543afbdc8e59 36913:da2977e674a3
     7 
     7 
     8 from __future__ import absolute_import, print_function
     8 from __future__ import absolute_import, print_function
     9 
     9 
    10 import os
    10 import os
    11 import re
    11 import re
    12 import types
       
    13 
    12 
    14 from .i18n import _
    13 from .i18n import _
    15 from . import (
    14 from . import (
    16     color,
    15     color,
    17     config,
    16     config,
    25     revset as revsetmod,
    24     revset as revsetmod,
    26     revsetlang,
    25     revsetlang,
    27     scmutil,
    26     scmutil,
    28     templatefilters,
    27     templatefilters,
    29     templatekw,
    28     templatekw,
       
    29     templateutil,
    30     util,
    30     util,
    31 )
    31 )
    32 from .utils import dateutil
    32 from .utils import dateutil
    33 
    33 
    34 class ResourceUnavailable(error.Abort):
    34 evalrawexp = templateutil.evalrawexp
    35     pass
    35 evalfuncarg = templateutil.evalfuncarg
    36 
    36 evalboolean = templateutil.evalboolean
    37 class TemplateNotFound(error.Abort):
    37 evalinteger = templateutil.evalinteger
    38     pass
    38 evalstring = templateutil.evalstring
       
    39 evalstringliteral = templateutil.evalstringliteral
       
    40 evalastype = templateutil.evalastype
    39 
    41 
    40 # template parsing
    42 # template parsing
    41 
    43 
    42 elements = {
    44 elements = {
    43     # token-type: binding-strength, primary, prefix, infix, suffix
    45     # token-type: binding-strength, primary, prefix, infix, suffix
   359         # even if it exists in mapping. this allows us to override mapping
   361         # even if it exists in mapping. this allows us to override mapping
   360         # by web templates, e.g. 'changelogtag' is redefined in map file.
   362         # by web templates, e.g. 'changelogtag' is redefined in map file.
   361         return context._load(exp[1])
   363         return context._load(exp[1])
   362     raise error.ParseError(_("expected template specifier"))
   364     raise error.ParseError(_("expected template specifier"))
   363 
   365 
   364 def findsymbolicname(arg):
       
   365     """Find symbolic name for the given compiled expression; returns None
       
   366     if nothing found reliably"""
       
   367     while True:
       
   368         func, data = arg
       
   369         if func is runsymbol:
       
   370             return data
       
   371         elif func is runfilter:
       
   372             arg = data[0]
       
   373         else:
       
   374             return None
       
   375 
       
   376 def evalrawexp(context, mapping, arg):
       
   377     """Evaluate given argument as a bare template object which may require
       
   378     further processing (such as folding generator of strings)"""
       
   379     func, data = arg
       
   380     return func(context, mapping, data)
       
   381 
       
   382 def evalfuncarg(context, mapping, arg):
       
   383     """Evaluate given argument as value type"""
       
   384     thing = evalrawexp(context, mapping, arg)
       
   385     thing = templatekw.unwrapvalue(thing)
       
   386     # evalrawexp() may return string, generator of strings or arbitrary object
       
   387     # such as date tuple, but filter does not want generator.
       
   388     if isinstance(thing, types.GeneratorType):
       
   389         thing = stringify(thing)
       
   390     return thing
       
   391 
       
   392 def evalboolean(context, mapping, arg):
       
   393     """Evaluate given argument as boolean, but also takes boolean literals"""
       
   394     func, data = arg
       
   395     if func is runsymbol:
       
   396         thing = func(context, mapping, data, default=None)
       
   397         if thing is None:
       
   398             # not a template keyword, takes as a boolean literal
       
   399             thing = util.parsebool(data)
       
   400     else:
       
   401         thing = func(context, mapping, data)
       
   402     thing = templatekw.unwrapvalue(thing)
       
   403     if isinstance(thing, bool):
       
   404         return thing
       
   405     # other objects are evaluated as strings, which means 0 is True, but
       
   406     # empty dict/list should be False as they are expected to be ''
       
   407     return bool(stringify(thing))
       
   408 
       
   409 def evalinteger(context, mapping, arg, err=None):
       
   410     v = evalfuncarg(context, mapping, arg)
       
   411     try:
       
   412         return int(v)
       
   413     except (TypeError, ValueError):
       
   414         raise error.ParseError(err or _('not an integer'))
       
   415 
       
   416 def evalstring(context, mapping, arg):
       
   417     return stringify(evalrawexp(context, mapping, arg))
       
   418 
       
   419 def evalstringliteral(context, mapping, arg):
       
   420     """Evaluate given argument as string template, but returns symbol name
       
   421     if it is unknown"""
       
   422     func, data = arg
       
   423     if func is runsymbol:
       
   424         thing = func(context, mapping, data, default=data)
       
   425     else:
       
   426         thing = func(context, mapping, data)
       
   427     return stringify(thing)
       
   428 
       
   429 _evalfuncbytype = {
       
   430     bool: evalboolean,
       
   431     bytes: evalstring,
       
   432     int: evalinteger,
       
   433 }
       
   434 
       
   435 def evalastype(context, mapping, arg, typ):
       
   436     """Evaluate given argument and coerce its type"""
       
   437     try:
       
   438         f = _evalfuncbytype[typ]
       
   439     except KeyError:
       
   440         raise error.ProgrammingError('invalid type specified: %r' % typ)
       
   441     return f(context, mapping, arg)
       
   442 
       
   443 def runinteger(context, mapping, data):
       
   444     return int(data)
       
   445 
       
   446 def runstring(context, mapping, data):
       
   447     return data
       
   448 
       
   449 def _recursivesymbolblocker(key):
       
   450     def showrecursion(**args):
       
   451         raise error.Abort(_("recursive reference '%s' in template") % key)
       
   452     return showrecursion
       
   453 
       
   454 def _runrecursivesymbol(context, mapping, key):
   366 def _runrecursivesymbol(context, mapping, key):
   455     raise error.Abort(_("recursive reference '%s' in template") % key)
   367     raise error.Abort(_("recursive reference '%s' in template") % key)
   456 
   368 
   457 def runsymbol(context, mapping, key, default=''):
       
   458     v = context.symbol(mapping, key)
       
   459     if v is None:
       
   460         # put poison to cut recursion. we can't move this to parsing phase
       
   461         # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
       
   462         safemapping = mapping.copy()
       
   463         safemapping[key] = _recursivesymbolblocker(key)
       
   464         try:
       
   465             v = context.process(key, safemapping)
       
   466         except TemplateNotFound:
       
   467             v = default
       
   468     if callable(v) and getattr(v, '_requires', None) is None:
       
   469         # old templatekw: expand all keywords and resources
       
   470         props = context._resources.copy()
       
   471         props.update(mapping)
       
   472         return v(**pycompat.strkwargs(props))
       
   473     if callable(v):
       
   474         # new templatekw
       
   475         try:
       
   476             return v(context, mapping)
       
   477         except ResourceUnavailable:
       
   478             # unsupported keyword is mapped to empty just like unknown keyword
       
   479             return None
       
   480     return v
       
   481 
       
   482 def buildtemplate(exp, context):
   369 def buildtemplate(exp, context):
   483     ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
   370     ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
   484     return (runtemplate, ctmpl)
   371     return (templateutil.runtemplate, ctmpl)
   485 
       
   486 def runtemplate(context, mapping, template):
       
   487     for arg in template:
       
   488         yield evalrawexp(context, mapping, arg)
       
   489 
   372 
   490 def buildfilter(exp, context):
   373 def buildfilter(exp, context):
   491     n = getsymbol(exp[2])
   374     n = getsymbol(exp[2])
   492     if n in context._filters:
   375     if n in context._filters:
   493         filt = context._filters[n]
   376         filt = context._filters[n]
   494         arg = compileexp(exp[1], context, methods)
   377         arg = compileexp(exp[1], context, methods)
   495         return (runfilter, (arg, filt))
   378         return (templateutil.runfilter, (arg, filt))
   496     if n in context._funcs:
   379     if n in context._funcs:
   497         f = context._funcs[n]
   380         f = context._funcs[n]
   498         args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
   381         args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
   499         return (f, args)
   382         return (f, args)
   500     raise error.ParseError(_("unknown function '%s'") % n)
   383     raise error.ParseError(_("unknown function '%s'") % n)
   501 
   384 
   502 def runfilter(context, mapping, data):
       
   503     arg, filt = data
       
   504     thing = evalfuncarg(context, mapping, arg)
       
   505     try:
       
   506         return filt(thing)
       
   507     except (ValueError, AttributeError, TypeError):
       
   508         sym = findsymbolicname(arg)
       
   509         if sym:
       
   510             msg = (_("template filter '%s' is not compatible with keyword '%s'")
       
   511                    % (pycompat.sysbytes(filt.__name__), sym))
       
   512         else:
       
   513             msg = (_("incompatible use of template filter '%s'")
       
   514                    % pycompat.sysbytes(filt.__name__))
       
   515         raise error.Abort(msg)
       
   516 
       
   517 def buildmap(exp, context):
   385 def buildmap(exp, context):
   518     darg = compileexp(exp[1], context, methods)
   386     darg = compileexp(exp[1], context, methods)
   519     targ = gettemplate(exp[2], context)
   387     targ = gettemplate(exp[2], context)
   520     return (runmap, (darg, targ))
   388     return (templateutil.runmap, (darg, targ))
   521 
       
   522 def runmap(context, mapping, data):
       
   523     darg, targ = data
       
   524     d = evalrawexp(context, mapping, darg)
       
   525     if util.safehasattr(d, 'itermaps'):
       
   526         diter = d.itermaps()
       
   527     else:
       
   528         try:
       
   529             diter = iter(d)
       
   530         except TypeError:
       
   531             sym = findsymbolicname(darg)
       
   532             if sym:
       
   533                 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
       
   534             else:
       
   535                 raise error.ParseError(_("%r is not iterable") % d)
       
   536 
       
   537     for i, v in enumerate(diter):
       
   538         lm = mapping.copy()
       
   539         lm['index'] = i
       
   540         if isinstance(v, dict):
       
   541             lm.update(v)
       
   542             lm['originalnode'] = mapping.get('node')
       
   543             yield evalrawexp(context, lm, targ)
       
   544         else:
       
   545             # v is not an iterable of dicts, this happen when 'key'
       
   546             # has been fully expanded already and format is useless.
       
   547             # If so, return the expanded value.
       
   548             yield v
       
   549 
   389 
   550 def buildmember(exp, context):
   390 def buildmember(exp, context):
   551     darg = compileexp(exp[1], context, methods)
   391     darg = compileexp(exp[1], context, methods)
   552     memb = getsymbol(exp[2])
   392     memb = getsymbol(exp[2])
   553     return (runmember, (darg, memb))
   393     return (templateutil.runmember, (darg, memb))
   554 
       
   555 def runmember(context, mapping, data):
       
   556     darg, memb = data
       
   557     d = evalrawexp(context, mapping, darg)
       
   558     if util.safehasattr(d, 'tomap'):
       
   559         lm = mapping.copy()
       
   560         lm.update(d.tomap())
       
   561         return runsymbol(context, lm, memb)
       
   562     if util.safehasattr(d, 'get'):
       
   563         return _getdictitem(d, memb)
       
   564 
       
   565     sym = findsymbolicname(darg)
       
   566     if sym:
       
   567         raise error.ParseError(_("keyword '%s' has no member") % sym)
       
   568     else:
       
   569         raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
       
   570 
   394 
   571 def buildnegate(exp, context):
   395 def buildnegate(exp, context):
   572     arg = compileexp(exp[1], context, exprmethods)
   396     arg = compileexp(exp[1], context, exprmethods)
   573     return (runnegate, arg)
   397     return (templateutil.runnegate, arg)
   574 
       
   575 def runnegate(context, mapping, data):
       
   576     data = evalinteger(context, mapping, data,
       
   577                        _('negation needs an integer argument'))
       
   578     return -data
       
   579 
   398 
   580 def buildarithmetic(exp, context, func):
   399 def buildarithmetic(exp, context, func):
   581     left = compileexp(exp[1], context, exprmethods)
   400     left = compileexp(exp[1], context, exprmethods)
   582     right = compileexp(exp[2], context, exprmethods)
   401     right = compileexp(exp[2], context, exprmethods)
   583     return (runarithmetic, (func, left, right))
   402     return (templateutil.runarithmetic, (func, left, right))
   584 
       
   585 def runarithmetic(context, mapping, data):
       
   586     func, left, right = data
       
   587     left = evalinteger(context, mapping, left,
       
   588                        _('arithmetic only defined on integers'))
       
   589     right = evalinteger(context, mapping, right,
       
   590                         _('arithmetic only defined on integers'))
       
   591     try:
       
   592         return func(left, right)
       
   593     except ZeroDivisionError:
       
   594         raise error.Abort(_('division by zero is not defined'))
       
   595 
   403 
   596 def buildfunc(exp, context):
   404 def buildfunc(exp, context):
   597     n = getsymbol(exp[1])
   405     n = getsymbol(exp[1])
   598     if n in context._funcs:
   406     if n in context._funcs:
   599         f = context._funcs[n]
   407         f = context._funcs[n]
   602     if n in context._filters:
   410     if n in context._filters:
   603         args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
   411         args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
   604         if len(args) != 1:
   412         if len(args) != 1:
   605             raise error.ParseError(_("filter %s expects one argument") % n)
   413             raise error.ParseError(_("filter %s expects one argument") % n)
   606         f = context._filters[n]
   414         f = context._filters[n]
   607         return (runfilter, (args[0], f))
   415         return (templateutil.runfilter, (args[0], f))
   608     raise error.ParseError(_("unknown function '%s'") % n)
   416     raise error.ParseError(_("unknown function '%s'") % n)
   609 
   417 
   610 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
   418 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
   611     """Compile parsed tree of function arguments into list or dict of
   419     """Compile parsed tree of function arguments into list or dict of
   612     (func, data) pairs
   420     (func, data) pairs
   679     """Construct a dict from key-value pairs. A key may be omitted if
   487     """Construct a dict from key-value pairs. A key may be omitted if
   680     a value expression can provide an unambiguous name."""
   488     a value expression can provide an unambiguous name."""
   681     data = util.sortdict()
   489     data = util.sortdict()
   682 
   490 
   683     for v in args['args']:
   491     for v in args['args']:
   684         k = findsymbolicname(v)
   492         k = templateutil.findsymbolicname(v)
   685         if not k:
   493         if not k:
   686             raise error.ParseError(_('dict key cannot be inferred'))
   494             raise error.ParseError(_('dict key cannot be inferred'))
   687         if k in data or k in args['kwargs']:
   495         if k in data or k in args['kwargs']:
   688             raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
   496             raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
   689         data[k] = evalfuncarg(context, mapping, v)
   497         data[k] = evalfuncarg(context, mapping, v)
   846     if not util.safehasattr(dictarg, 'get'):
   654     if not util.safehasattr(dictarg, 'get'):
   847         # i18n: "get" is a keyword
   655         # i18n: "get" is a keyword
   848         raise error.ParseError(_("get() expects a dict as first argument"))
   656         raise error.ParseError(_("get() expects a dict as first argument"))
   849 
   657 
   850     key = evalfuncarg(context, mapping, args[1])
   658     key = evalfuncarg(context, mapping, args[1])
   851     return _getdictitem(dictarg, key)
   659     return templateutil.getdictitem(dictarg, key)
   852 
       
   853 def _getdictitem(dictarg, key):
       
   854     val = dictarg.get(key)
       
   855     if val is None:
       
   856         return
       
   857     return templatekw.wraphybridvalue(dictarg, key, val)
       
   858 
   660 
   859 @templatefunc('if(expr, then[, else])')
   661 @templatefunc('if(expr, then[, else])')
   860 def if_(context, mapping, args):
   662 def if_(context, mapping, args):
   861     """Conditionally execute based on the result of
   663     """Conditionally execute based on the result of
   862     an expression."""
   664     an expression."""
  1029     if not len(args) == 2:
   831     if not len(args) == 2:
  1030         # i18n: "mod" is a keyword
   832         # i18n: "mod" is a keyword
  1031         raise error.ParseError(_("mod expects two arguments"))
   833         raise error.ParseError(_("mod expects two arguments"))
  1032 
   834 
  1033     func = lambda a, b: a % b
   835     func = lambda a, b: a % b
  1034     return runarithmetic(context, mapping, (func, args[0], args[1]))
   836     return templateutil.runarithmetic(context, mapping,
       
   837                                       (func, args[0], args[1]))
  1035 
   838 
  1036 @templatefunc('obsfateoperations(markers)')
   839 @templatefunc('obsfateoperations(markers)')
  1037 def obsfateoperations(context, mapping, args):
   840 def obsfateoperations(context, mapping, args):
  1038     """Compute obsfate related information based on markers (EXPERIMENTAL)"""
   841     """Compute obsfate related information based on markers (EXPERIMENTAL)"""
  1039     if len(args) != 1:
   842     if len(args) != 1:
  1271     else:
  1074     else:
  1272         return tokens[num]
  1075         return tokens[num]
  1273 
  1076 
  1274 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
  1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
  1275 exprmethods = {
  1078 exprmethods = {
  1276     "integer": lambda e, c: (runinteger, e[1]),
  1079     "integer": lambda e, c: (templateutil.runinteger, e[1]),
  1277     "string": lambda e, c: (runstring, e[1]),
  1080     "string": lambda e, c: (templateutil.runstring, e[1]),
  1278     "symbol": lambda e, c: (runsymbol, e[1]),
  1081     "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
  1279     "template": buildtemplate,
  1082     "template": buildtemplate,
  1280     "group": lambda e, c: compileexp(e[1], c, exprmethods),
  1083     "group": lambda e, c: compileexp(e[1], c, exprmethods),
  1281     ".": buildmember,
  1084     ".": buildmember,
  1282     "|": buildfilter,
  1085     "|": buildfilter,
  1283     "%": buildmap,
  1086     "%": buildmap,
  1402         if key in self._resources:
  1205         if key in self._resources:
  1403             v = mapping.get(key)
  1206             v = mapping.get(key)
  1404         if v is None:
  1207         if v is None:
  1405             v = self._resources.get(key)
  1208             v = self._resources.get(key)
  1406         if v is None:
  1209         if v is None:
  1407             raise ResourceUnavailable(_('template resource not available: %s')
  1210             raise templateutil.ResourceUnavailable(
  1408                                       % key)
  1211                 _('template resource not available: %s') % key)
  1409         return v
  1212         return v
  1410 
  1213 
  1411     def _load(self, t):
  1214     def _load(self, t):
  1412         '''load, parse, and cache a template'''
  1215         '''load, parse, and cache a template'''
  1413         if t not in self._cache:
  1216         if t not in self._cache:
  1550         '''Get the template for the given template name. Use a local cache.'''
  1353         '''Get the template for the given template name. Use a local cache.'''
  1551         if t not in self.cache:
  1354         if t not in self.cache:
  1552             try:
  1355             try:
  1553                 self.cache[t] = util.readfile(self.map[t][1])
  1356                 self.cache[t] = util.readfile(self.map[t][1])
  1554             except KeyError as inst:
  1357             except KeyError as inst:
  1555                 raise TemplateNotFound(_('"%s" not in template map') %
  1358                 raise templateutil.TemplateNotFound(
  1556                                        inst.args[0])
  1359                     _('"%s" not in template map') % inst.args[0])
  1557             except IOError as inst:
  1360             except IOError as inst:
  1558                 reason = (_('template file %s: %s')
  1361                 reason = (_('template file %s: %s')
  1559                           % (self.map[t][1], util.forcebytestr(inst.args[1])))
  1362                           % (self.map[t][1], util.forcebytestr(inst.args[1])))
  1560                 raise IOError(inst.args[0], encoding.strfromlocal(reason))
  1363                 raise IOError(inst.args[0], encoding.strfromlocal(reason))
  1561         return self.cache[t]
  1364         return self.cache[t]