comparison 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
comparison
equal deleted inserted replaced
36912:543afbdc8e59 36913:da2977e674a3
1 # templateutil.py - utility for template evaluation
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from __future__ import absolute_import
9
10 import types
11
12 from .i18n import _
13 from . import (
14 error,
15 pycompat,
16 templatefilters,
17 templatekw,
18 util,
19 )
20
21 class ResourceUnavailable(error.Abort):
22 pass
23
24 class TemplateNotFound(error.Abort):
25 pass
26
27 def findsymbolicname(arg):
28 """Find symbolic name for the given compiled expression; returns None
29 if nothing found reliably"""
30 while True:
31 func, data = arg
32 if func is runsymbol:
33 return data
34 elif func is runfilter:
35 arg = data[0]
36 else:
37 return None
38
39 def evalrawexp(context, mapping, arg):
40 """Evaluate given argument as a bare template object which may require
41 further processing (such as folding generator of strings)"""
42 func, data = arg
43 return func(context, mapping, data)
44
45 def evalfuncarg(context, mapping, arg):
46 """Evaluate given argument as value type"""
47 thing = evalrawexp(context, mapping, arg)
48 thing = templatekw.unwrapvalue(thing)
49 # evalrawexp() may return string, generator of strings or arbitrary object
50 # such as date tuple, but filter does not want generator.
51 if isinstance(thing, types.GeneratorType):
52 thing = stringify(thing)
53 return thing
54
55 def evalboolean(context, mapping, arg):
56 """Evaluate given argument as boolean, but also takes boolean literals"""
57 func, data = arg
58 if func is runsymbol:
59 thing = func(context, mapping, data, default=None)
60 if thing is None:
61 # not a template keyword, takes as a boolean literal
62 thing = util.parsebool(data)
63 else:
64 thing = func(context, mapping, data)
65 thing = templatekw.unwrapvalue(thing)
66 if isinstance(thing, bool):
67 return thing
68 # other objects are evaluated as strings, which means 0 is True, but
69 # empty dict/list should be False as they are expected to be ''
70 return bool(stringify(thing))
71
72 def evalinteger(context, mapping, arg, err=None):
73 v = evalfuncarg(context, mapping, arg)
74 try:
75 return int(v)
76 except (TypeError, ValueError):
77 raise error.ParseError(err or _('not an integer'))
78
79 def evalstring(context, mapping, arg):
80 return stringify(evalrawexp(context, mapping, arg))
81
82 def evalstringliteral(context, mapping, arg):
83 """Evaluate given argument as string template, but returns symbol name
84 if it is unknown"""
85 func, data = arg
86 if func is runsymbol:
87 thing = func(context, mapping, data, default=data)
88 else:
89 thing = func(context, mapping, data)
90 return stringify(thing)
91
92 _evalfuncbytype = {
93 bool: evalboolean,
94 bytes: evalstring,
95 int: evalinteger,
96 }
97
98 def evalastype(context, mapping, arg, typ):
99 """Evaluate given argument and coerce its type"""
100 try:
101 f = _evalfuncbytype[typ]
102 except KeyError:
103 raise error.ProgrammingError('invalid type specified: %r' % typ)
104 return f(context, mapping, arg)
105
106 def runinteger(context, mapping, data):
107 return int(data)
108
109 def runstring(context, mapping, data):
110 return data
111
112 def _recursivesymbolblocker(key):
113 def showrecursion(**args):
114 raise error.Abort(_("recursive reference '%s' in template") % key)
115 return showrecursion
116
117 def runsymbol(context, mapping, key, default=''):
118 v = context.symbol(mapping, key)
119 if v is None:
120 # put poison to cut recursion. we can't move this to parsing phase
121 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
122 safemapping = mapping.copy()
123 safemapping[key] = _recursivesymbolblocker(key)
124 try:
125 v = context.process(key, safemapping)
126 except TemplateNotFound:
127 v = default
128 if callable(v) and getattr(v, '_requires', None) is None:
129 # old templatekw: expand all keywords and resources
130 props = context._resources.copy()
131 props.update(mapping)
132 return v(**pycompat.strkwargs(props))
133 if callable(v):
134 # new templatekw
135 try:
136 return v(context, mapping)
137 except ResourceUnavailable:
138 # unsupported keyword is mapped to empty just like unknown keyword
139 return None
140 return v
141
142 def runtemplate(context, mapping, template):
143 for arg in template:
144 yield evalrawexp(context, mapping, arg)
145
146 def runfilter(context, mapping, data):
147 arg, filt = data
148 thing = evalfuncarg(context, mapping, arg)
149 try:
150 return filt(thing)
151 except (ValueError, AttributeError, TypeError):
152 sym = findsymbolicname(arg)
153 if sym:
154 msg = (_("template filter '%s' is not compatible with keyword '%s'")
155 % (pycompat.sysbytes(filt.__name__), sym))
156 else:
157 msg = (_("incompatible use of template filter '%s'")
158 % pycompat.sysbytes(filt.__name__))
159 raise error.Abort(msg)
160
161 def runmap(context, mapping, data):
162 darg, targ = data
163 d = evalrawexp(context, mapping, darg)
164 if util.safehasattr(d, 'itermaps'):
165 diter = d.itermaps()
166 else:
167 try:
168 diter = iter(d)
169 except TypeError:
170 sym = findsymbolicname(darg)
171 if sym:
172 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
173 else:
174 raise error.ParseError(_("%r is not iterable") % d)
175
176 for i, v in enumerate(diter):
177 lm = mapping.copy()
178 lm['index'] = i
179 if isinstance(v, dict):
180 lm.update(v)
181 lm['originalnode'] = mapping.get('node')
182 yield evalrawexp(context, lm, targ)
183 else:
184 # v is not an iterable of dicts, this happen when 'key'
185 # has been fully expanded already and format is useless.
186 # If so, return the expanded value.
187 yield v
188
189 def runmember(context, mapping, data):
190 darg, memb = data
191 d = evalrawexp(context, mapping, darg)
192 if util.safehasattr(d, 'tomap'):
193 lm = mapping.copy()
194 lm.update(d.tomap())
195 return runsymbol(context, lm, memb)
196 if util.safehasattr(d, 'get'):
197 return getdictitem(d, memb)
198
199 sym = findsymbolicname(darg)
200 if sym:
201 raise error.ParseError(_("keyword '%s' has no member") % sym)
202 else:
203 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
204
205 def runnegate(context, mapping, data):
206 data = evalinteger(context, mapping, data,
207 _('negation needs an integer argument'))
208 return -data
209
210 def runarithmetic(context, mapping, data):
211 func, left, right = data
212 left = evalinteger(context, mapping, left,
213 _('arithmetic only defined on integers'))
214 right = evalinteger(context, mapping, right,
215 _('arithmetic only defined on integers'))
216 try:
217 return func(left, right)
218 except ZeroDivisionError:
219 raise error.Abort(_('division by zero is not defined'))
220
221 def getdictitem(dictarg, key):
222 val = dictarg.get(key)
223 if val is None:
224 return
225 return templatekw.wraphybridvalue(dictarg, key, val)
226
227 stringify = templatefilters.stringify