##// END OF EJS Templates
templater: extract private function to evaluate generator to byte string
Yuya Nishihara -
r37296:671a01cd default
parent child Browse files
Show More
@@ -1,533 +1,537 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import abc
10 import abc
11 import types
11 import types
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 )
18 )
19 from .utils import (
19 from .utils import (
20 dateutil,
20 dateutil,
21 stringutil,
21 stringutil,
22 )
22 )
23
23
24 class ResourceUnavailable(error.Abort):
24 class ResourceUnavailable(error.Abort):
25 pass
25 pass
26
26
27 class TemplateNotFound(error.Abort):
27 class TemplateNotFound(error.Abort):
28 pass
28 pass
29
29
30 class wrapped(object):
30 class wrapped(object):
31 """Object requiring extra conversion prior to displaying or processing
31 """Object requiring extra conversion prior to displaying or processing
32 as value"""
32 as value"""
33
33
34 __metaclass__ = abc.ABCMeta
34 __metaclass__ = abc.ABCMeta
35
35
36 @abc.abstractmethod
36 @abc.abstractmethod
37 def show(self, context, mapping):
37 def show(self, context, mapping):
38 """Return a bytes or (possibly nested) generator of bytes representing
38 """Return a bytes or (possibly nested) generator of bytes representing
39 the underlying object
39 the underlying object
40
40
41 A pre-configured template may be rendered if the underlying object is
41 A pre-configured template may be rendered if the underlying object is
42 not printable.
42 not printable.
43 """
43 """
44
44
45 # stub for representing a date type; may be a real date type that can
45 # stub for representing a date type; may be a real date type that can
46 # provide a readable string value
46 # provide a readable string value
47 class date(object):
47 class date(object):
48 pass
48 pass
49
49
50 class hybrid(wrapped):
50 class hybrid(wrapped):
51 """Wrapper for list or dict to support legacy template
51 """Wrapper for list or dict to support legacy template
52
52
53 This class allows us to handle both:
53 This class allows us to handle both:
54 - "{files}" (legacy command-line-specific list hack) and
54 - "{files}" (legacy command-line-specific list hack) and
55 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
55 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
56 and to access raw values:
56 and to access raw values:
57 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
57 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
58 - "{get(extras, key)}"
58 - "{get(extras, key)}"
59 - "{files|json}"
59 - "{files|json}"
60 """
60 """
61
61
62 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
62 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
63 if gen is not None:
63 if gen is not None:
64 self._gen = gen # generator or function returning generator
64 self._gen = gen # generator or function returning generator
65 self._values = values
65 self._values = values
66 self._makemap = makemap
66 self._makemap = makemap
67 self.joinfmt = joinfmt
67 self.joinfmt = joinfmt
68 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
68 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
69
69
70 def _gen(self):
70 def _gen(self):
71 """Default generator to stringify this as {join(self, ' ')}"""
71 """Default generator to stringify this as {join(self, ' ')}"""
72 for i, x in enumerate(self._values):
72 for i, x in enumerate(self._values):
73 if i > 0:
73 if i > 0:
74 yield ' '
74 yield ' '
75 yield self.joinfmt(x)
75 yield self.joinfmt(x)
76 def itermaps(self):
76 def itermaps(self):
77 makemap = self._makemap
77 makemap = self._makemap
78 for x in self._values:
78 for x in self._values:
79 yield makemap(x)
79 yield makemap(x)
80
80
81 def show(self, context, mapping):
81 def show(self, context, mapping):
82 # TODO: switch gen to (context, mapping) API?
82 # TODO: switch gen to (context, mapping) API?
83 gen = self._gen
83 gen = self._gen
84 if callable(gen):
84 if callable(gen):
85 return gen()
85 return gen()
86 return gen
86 return gen
87
87
88 def __contains__(self, x):
88 def __contains__(self, x):
89 return x in self._values
89 return x in self._values
90 def __getitem__(self, key):
90 def __getitem__(self, key):
91 return self._values[key]
91 return self._values[key]
92 def __len__(self):
92 def __len__(self):
93 return len(self._values)
93 return len(self._values)
94 def __iter__(self):
94 def __iter__(self):
95 return iter(self._values)
95 return iter(self._values)
96 def __getattr__(self, name):
96 def __getattr__(self, name):
97 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
97 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
98 r'itervalues', r'keys', r'values'):
98 r'itervalues', r'keys', r'values'):
99 raise AttributeError(name)
99 raise AttributeError(name)
100 return getattr(self._values, name)
100 return getattr(self._values, name)
101
101
102 class mappable(wrapped):
102 class mappable(wrapped):
103 """Wrapper for non-list/dict object to support map operation
103 """Wrapper for non-list/dict object to support map operation
104
104
105 This class allows us to handle both:
105 This class allows us to handle both:
106 - "{manifest}"
106 - "{manifest}"
107 - "{manifest % '{rev}:{node}'}"
107 - "{manifest % '{rev}:{node}'}"
108 - "{manifest.rev}"
108 - "{manifest.rev}"
109
109
110 Unlike a hybrid, this does not simulate the behavior of the underling
110 Unlike a hybrid, this does not simulate the behavior of the underling
111 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
111 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
112 the inner object.
112 the inner object.
113 """
113 """
114
114
115 def __init__(self, gen, key, value, makemap):
115 def __init__(self, gen, key, value, makemap):
116 self._gen = gen # generator or function returning generator
116 self._gen = gen # generator or function returning generator
117 self._key = key
117 self._key = key
118 self._value = value # may be generator of strings
118 self._value = value # may be generator of strings
119 self._makemap = makemap
119 self._makemap = makemap
120
120
121 def tomap(self):
121 def tomap(self):
122 return self._makemap(self._key)
122 return self._makemap(self._key)
123
123
124 def itermaps(self):
124 def itermaps(self):
125 yield self.tomap()
125 yield self.tomap()
126
126
127 def show(self, context, mapping):
127 def show(self, context, mapping):
128 # TODO: switch gen to (context, mapping) API?
128 # TODO: switch gen to (context, mapping) API?
129 gen = self._gen
129 gen = self._gen
130 if gen is None:
130 if gen is None:
131 return pycompat.bytestr(self._value)
131 return pycompat.bytestr(self._value)
132 if callable(gen):
132 if callable(gen):
133 return gen()
133 return gen()
134 return gen
134 return gen
135
135
136 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
136 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
137 """Wrap data to support both dict-like and string-like operations"""
137 """Wrap data to support both dict-like and string-like operations"""
138 prefmt = pycompat.identity
138 prefmt = pycompat.identity
139 if fmt is None:
139 if fmt is None:
140 fmt = '%s=%s'
140 fmt = '%s=%s'
141 prefmt = pycompat.bytestr
141 prefmt = pycompat.bytestr
142 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
142 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
143 lambda k: fmt % (prefmt(k), prefmt(data[k])))
143 lambda k: fmt % (prefmt(k), prefmt(data[k])))
144
144
145 def hybridlist(data, name, fmt=None, gen=None):
145 def hybridlist(data, name, fmt=None, gen=None):
146 """Wrap data to support both list-like and string-like operations"""
146 """Wrap data to support both list-like and string-like operations"""
147 prefmt = pycompat.identity
147 prefmt = pycompat.identity
148 if fmt is None:
148 if fmt is None:
149 fmt = '%s'
149 fmt = '%s'
150 prefmt = pycompat.bytestr
150 prefmt = pycompat.bytestr
151 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
151 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
152
152
153 def unwraphybrid(context, mapping, thing):
153 def unwraphybrid(context, mapping, thing):
154 """Return an object which can be stringified possibly by using a legacy
154 """Return an object which can be stringified possibly by using a legacy
155 template"""
155 template"""
156 if not isinstance(thing, wrapped):
156 if not isinstance(thing, wrapped):
157 return thing
157 return thing
158 return thing.show(context, mapping)
158 return thing.show(context, mapping)
159
159
160 def unwrapvalue(context, mapping, thing):
160 def unwrapvalue(context, mapping, thing):
161 """Move the inner value object out of the wrapper"""
161 """Move the inner value object out of the wrapper"""
162 if not util.safehasattr(thing, '_value'):
162 if not util.safehasattr(thing, '_value'):
163 return thing
163 return thing
164 return thing._value
164 return thing._value
165
165
166 def wraphybridvalue(container, key, value):
166 def wraphybridvalue(container, key, value):
167 """Wrap an element of hybrid container to be mappable
167 """Wrap an element of hybrid container to be mappable
168
168
169 The key is passed to the makemap function of the given container, which
169 The key is passed to the makemap function of the given container, which
170 should be an item generated by iter(container).
170 should be an item generated by iter(container).
171 """
171 """
172 makemap = getattr(container, '_makemap', None)
172 makemap = getattr(container, '_makemap', None)
173 if makemap is None:
173 if makemap is None:
174 return value
174 return value
175 if util.safehasattr(value, '_makemap'):
175 if util.safehasattr(value, '_makemap'):
176 # a nested hybrid list/dict, which has its own way of map operation
176 # a nested hybrid list/dict, which has its own way of map operation
177 return value
177 return value
178 return mappable(None, key, value, makemap)
178 return mappable(None, key, value, makemap)
179
179
180 def compatdict(context, mapping, name, data, key='key', value='value',
180 def compatdict(context, mapping, name, data, key='key', value='value',
181 fmt=None, plural=None, separator=' '):
181 fmt=None, plural=None, separator=' '):
182 """Wrap data like hybriddict(), but also supports old-style list template
182 """Wrap data like hybriddict(), but also supports old-style list template
183
183
184 This exists for backward compatibility with the old-style template. Use
184 This exists for backward compatibility with the old-style template. Use
185 hybriddict() for new template keywords.
185 hybriddict() for new template keywords.
186 """
186 """
187 c = [{key: k, value: v} for k, v in data.iteritems()]
187 c = [{key: k, value: v} for k, v in data.iteritems()]
188 f = _showcompatlist(context, mapping, name, c, plural, separator)
188 f = _showcompatlist(context, mapping, name, c, plural, separator)
189 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
189 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
190
190
191 def compatlist(context, mapping, name, data, element=None, fmt=None,
191 def compatlist(context, mapping, name, data, element=None, fmt=None,
192 plural=None, separator=' '):
192 plural=None, separator=' '):
193 """Wrap data like hybridlist(), but also supports old-style list template
193 """Wrap data like hybridlist(), but also supports old-style list template
194
194
195 This exists for backward compatibility with the old-style template. Use
195 This exists for backward compatibility with the old-style template. Use
196 hybridlist() for new template keywords.
196 hybridlist() for new template keywords.
197 """
197 """
198 f = _showcompatlist(context, mapping, name, data, plural, separator)
198 f = _showcompatlist(context, mapping, name, data, plural, separator)
199 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
199 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
200
200
201 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
201 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
202 """Return a generator that renders old-style list template
202 """Return a generator that renders old-style list template
203
203
204 name is name of key in template map.
204 name is name of key in template map.
205 values is list of strings or dicts.
205 values is list of strings or dicts.
206 plural is plural of name, if not simply name + 's'.
206 plural is plural of name, if not simply name + 's'.
207 separator is used to join values as a string
207 separator is used to join values as a string
208
208
209 expansion works like this, given name 'foo'.
209 expansion works like this, given name 'foo'.
210
210
211 if values is empty, expand 'no_foos'.
211 if values is empty, expand 'no_foos'.
212
212
213 if 'foo' not in template map, return values as a string,
213 if 'foo' not in template map, return values as a string,
214 joined by 'separator'.
214 joined by 'separator'.
215
215
216 expand 'start_foos'.
216 expand 'start_foos'.
217
217
218 for each value, expand 'foo'. if 'last_foo' in template
218 for each value, expand 'foo'. if 'last_foo' in template
219 map, expand it instead of 'foo' for last key.
219 map, expand it instead of 'foo' for last key.
220
220
221 expand 'end_foos'.
221 expand 'end_foos'.
222 """
222 """
223 if not plural:
223 if not plural:
224 plural = name + 's'
224 plural = name + 's'
225 if not values:
225 if not values:
226 noname = 'no_' + plural
226 noname = 'no_' + plural
227 if context.preload(noname):
227 if context.preload(noname):
228 yield context.process(noname, mapping)
228 yield context.process(noname, mapping)
229 return
229 return
230 if not context.preload(name):
230 if not context.preload(name):
231 if isinstance(values[0], bytes):
231 if isinstance(values[0], bytes):
232 yield separator.join(values)
232 yield separator.join(values)
233 else:
233 else:
234 for v in values:
234 for v in values:
235 r = dict(v)
235 r = dict(v)
236 r.update(mapping)
236 r.update(mapping)
237 yield r
237 yield r
238 return
238 return
239 startname = 'start_' + plural
239 startname = 'start_' + plural
240 if context.preload(startname):
240 if context.preload(startname):
241 yield context.process(startname, mapping)
241 yield context.process(startname, mapping)
242 def one(v, tag=name):
242 def one(v, tag=name):
243 vmapping = {}
243 vmapping = {}
244 try:
244 try:
245 vmapping.update(v)
245 vmapping.update(v)
246 # Python 2 raises ValueError if the type of v is wrong. Python
246 # Python 2 raises ValueError if the type of v is wrong. Python
247 # 3 raises TypeError.
247 # 3 raises TypeError.
248 except (AttributeError, TypeError, ValueError):
248 except (AttributeError, TypeError, ValueError):
249 try:
249 try:
250 # Python 2 raises ValueError trying to destructure an e.g.
250 # Python 2 raises ValueError trying to destructure an e.g.
251 # bytes. Python 3 raises TypeError.
251 # bytes. Python 3 raises TypeError.
252 for a, b in v:
252 for a, b in v:
253 vmapping[a] = b
253 vmapping[a] = b
254 except (TypeError, ValueError):
254 except (TypeError, ValueError):
255 vmapping[name] = v
255 vmapping[name] = v
256 vmapping = context.overlaymap(mapping, vmapping)
256 vmapping = context.overlaymap(mapping, vmapping)
257 return context.process(tag, vmapping)
257 return context.process(tag, vmapping)
258 lastname = 'last_' + name
258 lastname = 'last_' + name
259 if context.preload(lastname):
259 if context.preload(lastname):
260 last = values.pop()
260 last = values.pop()
261 else:
261 else:
262 last = None
262 last = None
263 for v in values:
263 for v in values:
264 yield one(v)
264 yield one(v)
265 if last is not None:
265 if last is not None:
266 yield one(last, tag=lastname)
266 yield one(last, tag=lastname)
267 endname = 'end_' + plural
267 endname = 'end_' + plural
268 if context.preload(endname):
268 if context.preload(endname):
269 yield context.process(endname, mapping)
269 yield context.process(endname, mapping)
270
270
271 def flatten(context, mapping, thing):
271 def flatten(context, mapping, thing):
272 """Yield a single stream from a possibly nested set of iterators"""
272 """Yield a single stream from a possibly nested set of iterators"""
273 thing = unwraphybrid(context, mapping, thing)
273 thing = unwraphybrid(context, mapping, thing)
274 if isinstance(thing, bytes):
274 if isinstance(thing, bytes):
275 yield thing
275 yield thing
276 elif isinstance(thing, str):
276 elif isinstance(thing, str):
277 # We can only hit this on Python 3, and it's here to guard
277 # We can only hit this on Python 3, and it's here to guard
278 # against infinite recursion.
278 # against infinite recursion.
279 raise error.ProgrammingError('Mercurial IO including templates is done'
279 raise error.ProgrammingError('Mercurial IO including templates is done'
280 ' with bytes, not strings, got %r' % thing)
280 ' with bytes, not strings, got %r' % thing)
281 elif thing is None:
281 elif thing is None:
282 pass
282 pass
283 elif not util.safehasattr(thing, '__iter__'):
283 elif not util.safehasattr(thing, '__iter__'):
284 yield pycompat.bytestr(thing)
284 yield pycompat.bytestr(thing)
285 else:
285 else:
286 for i in thing:
286 for i in thing:
287 i = unwraphybrid(context, mapping, i)
287 i = unwraphybrid(context, mapping, i)
288 if isinstance(i, bytes):
288 if isinstance(i, bytes):
289 yield i
289 yield i
290 elif i is None:
290 elif i is None:
291 pass
291 pass
292 elif not util.safehasattr(i, '__iter__'):
292 elif not util.safehasattr(i, '__iter__'):
293 yield pycompat.bytestr(i)
293 yield pycompat.bytestr(i)
294 else:
294 else:
295 for j in flatten(context, mapping, i):
295 for j in flatten(context, mapping, i):
296 yield j
296 yield j
297
297
298 def stringify(context, mapping, thing):
298 def stringify(context, mapping, thing):
299 """Turn values into bytes by converting into text and concatenating them"""
299 """Turn values into bytes by converting into text and concatenating them"""
300 if isinstance(thing, bytes):
300 if isinstance(thing, bytes):
301 return thing # retain localstr to be round-tripped
301 return thing # retain localstr to be round-tripped
302 return b''.join(flatten(context, mapping, thing))
302 return b''.join(flatten(context, mapping, thing))
303
303
304 def findsymbolicname(arg):
304 def findsymbolicname(arg):
305 """Find symbolic name for the given compiled expression; returns None
305 """Find symbolic name for the given compiled expression; returns None
306 if nothing found reliably"""
306 if nothing found reliably"""
307 while True:
307 while True:
308 func, data = arg
308 func, data = arg
309 if func is runsymbol:
309 if func is runsymbol:
310 return data
310 return data
311 elif func is runfilter:
311 elif func is runfilter:
312 arg = data[0]
312 arg = data[0]
313 else:
313 else:
314 return None
314 return None
315
315
316 def _unthunk(context, mapping, thing):
317 """Evaluate a lazy byte string into value"""
318 if not isinstance(thing, types.GeneratorType):
319 return thing
320 return stringify(context, mapping, thing)
321
316 def evalrawexp(context, mapping, arg):
322 def evalrawexp(context, mapping, arg):
317 """Evaluate given argument as a bare template object which may require
323 """Evaluate given argument as a bare template object which may require
318 further processing (such as folding generator of strings)"""
324 further processing (such as folding generator of strings)"""
319 func, data = arg
325 func, data = arg
320 return func(context, mapping, data)
326 return func(context, mapping, data)
321
327
322 def evalfuncarg(context, mapping, arg):
328 def evalfuncarg(context, mapping, arg):
323 """Evaluate given argument as value type"""
329 """Evaluate given argument as value type"""
324 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
330 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
325
331
326 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
332 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
327 # is fixed. we can't do that right now because join() has to take a generator
333 # is fixed. we can't do that right now because join() has to take a generator
328 # of byte strings as it is, not a lazy byte string.
334 # of byte strings as it is, not a lazy byte string.
329 def _unwrapvalue(context, mapping, thing):
335 def _unwrapvalue(context, mapping, thing):
330 thing = unwrapvalue(context, mapping, thing)
336 thing = unwrapvalue(context, mapping, thing)
331 # evalrawexp() may return string, generator of strings or arbitrary object
337 # evalrawexp() may return string, generator of strings or arbitrary object
332 # such as date tuple, but filter does not want generator.
338 # such as date tuple, but filter does not want generator.
333 if isinstance(thing, types.GeneratorType):
339 return _unthunk(context, mapping, thing)
334 thing = stringify(context, mapping, thing)
335 return thing
336
340
337 def evalboolean(context, mapping, arg):
341 def evalboolean(context, mapping, arg):
338 """Evaluate given argument as boolean, but also takes boolean literals"""
342 """Evaluate given argument as boolean, but also takes boolean literals"""
339 func, data = arg
343 func, data = arg
340 if func is runsymbol:
344 if func is runsymbol:
341 thing = func(context, mapping, data, default=None)
345 thing = func(context, mapping, data, default=None)
342 if thing is None:
346 if thing is None:
343 # not a template keyword, takes as a boolean literal
347 # not a template keyword, takes as a boolean literal
344 thing = stringutil.parsebool(data)
348 thing = stringutil.parsebool(data)
345 else:
349 else:
346 thing = func(context, mapping, data)
350 thing = func(context, mapping, data)
347 thing = unwrapvalue(context, mapping, thing)
351 thing = unwrapvalue(context, mapping, thing)
348 if isinstance(thing, bool):
352 if isinstance(thing, bool):
349 return thing
353 return thing
350 # other objects are evaluated as strings, which means 0 is True, but
354 # other objects are evaluated as strings, which means 0 is True, but
351 # empty dict/list should be False as they are expected to be ''
355 # empty dict/list should be False as they are expected to be ''
352 return bool(stringify(context, mapping, thing))
356 return bool(stringify(context, mapping, thing))
353
357
354 def evaldate(context, mapping, arg, err=None):
358 def evaldate(context, mapping, arg, err=None):
355 """Evaluate given argument as a date tuple or a date string; returns
359 """Evaluate given argument as a date tuple or a date string; returns
356 a (unixtime, offset) tuple"""
360 a (unixtime, offset) tuple"""
357 thing = evalrawexp(context, mapping, arg)
361 thing = evalrawexp(context, mapping, arg)
358 return unwrapdate(context, mapping, thing, err)
362 return unwrapdate(context, mapping, thing, err)
359
363
360 def unwrapdate(context, mapping, thing, err=None):
364 def unwrapdate(context, mapping, thing, err=None):
361 thing = _unwrapvalue(context, mapping, thing)
365 thing = _unwrapvalue(context, mapping, thing)
362 try:
366 try:
363 return dateutil.parsedate(thing)
367 return dateutil.parsedate(thing)
364 except AttributeError:
368 except AttributeError:
365 raise error.ParseError(err or _('not a date tuple nor a string'))
369 raise error.ParseError(err or _('not a date tuple nor a string'))
366 except error.ParseError:
370 except error.ParseError:
367 if not err:
371 if not err:
368 raise
372 raise
369 raise error.ParseError(err)
373 raise error.ParseError(err)
370
374
371 def evalinteger(context, mapping, arg, err=None):
375 def evalinteger(context, mapping, arg, err=None):
372 thing = evalrawexp(context, mapping, arg)
376 thing = evalrawexp(context, mapping, arg)
373 return unwrapinteger(context, mapping, thing, err)
377 return unwrapinteger(context, mapping, thing, err)
374
378
375 def unwrapinteger(context, mapping, thing, err=None):
379 def unwrapinteger(context, mapping, thing, err=None):
376 thing = _unwrapvalue(context, mapping, thing)
380 thing = _unwrapvalue(context, mapping, thing)
377 try:
381 try:
378 return int(thing)
382 return int(thing)
379 except (TypeError, ValueError):
383 except (TypeError, ValueError):
380 raise error.ParseError(err or _('not an integer'))
384 raise error.ParseError(err or _('not an integer'))
381
385
382 def evalstring(context, mapping, arg):
386 def evalstring(context, mapping, arg):
383 return stringify(context, mapping, evalrawexp(context, mapping, arg))
387 return stringify(context, mapping, evalrawexp(context, mapping, arg))
384
388
385 def evalstringliteral(context, mapping, arg):
389 def evalstringliteral(context, mapping, arg):
386 """Evaluate given argument as string template, but returns symbol name
390 """Evaluate given argument as string template, but returns symbol name
387 if it is unknown"""
391 if it is unknown"""
388 func, data = arg
392 func, data = arg
389 if func is runsymbol:
393 if func is runsymbol:
390 thing = func(context, mapping, data, default=data)
394 thing = func(context, mapping, data, default=data)
391 else:
395 else:
392 thing = func(context, mapping, data)
396 thing = func(context, mapping, data)
393 return stringify(context, mapping, thing)
397 return stringify(context, mapping, thing)
394
398
395 _unwrapfuncbytype = {
399 _unwrapfuncbytype = {
396 None: _unwrapvalue,
400 None: _unwrapvalue,
397 bytes: stringify,
401 bytes: stringify,
398 date: unwrapdate,
402 date: unwrapdate,
399 int: unwrapinteger,
403 int: unwrapinteger,
400 }
404 }
401
405
402 def unwrapastype(context, mapping, thing, typ):
406 def unwrapastype(context, mapping, thing, typ):
403 """Move the inner value object out of the wrapper and coerce its type"""
407 """Move the inner value object out of the wrapper and coerce its type"""
404 try:
408 try:
405 f = _unwrapfuncbytype[typ]
409 f = _unwrapfuncbytype[typ]
406 except KeyError:
410 except KeyError:
407 raise error.ProgrammingError('invalid type specified: %r' % typ)
411 raise error.ProgrammingError('invalid type specified: %r' % typ)
408 return f(context, mapping, thing)
412 return f(context, mapping, thing)
409
413
410 def runinteger(context, mapping, data):
414 def runinteger(context, mapping, data):
411 return int(data)
415 return int(data)
412
416
413 def runstring(context, mapping, data):
417 def runstring(context, mapping, data):
414 return data
418 return data
415
419
416 def _recursivesymbolblocker(key):
420 def _recursivesymbolblocker(key):
417 def showrecursion(**args):
421 def showrecursion(**args):
418 raise error.Abort(_("recursive reference '%s' in template") % key)
422 raise error.Abort(_("recursive reference '%s' in template") % key)
419 return showrecursion
423 return showrecursion
420
424
421 def runsymbol(context, mapping, key, default=''):
425 def runsymbol(context, mapping, key, default=''):
422 v = context.symbol(mapping, key)
426 v = context.symbol(mapping, key)
423 if v is None:
427 if v is None:
424 # put poison to cut recursion. we can't move this to parsing phase
428 # put poison to cut recursion. we can't move this to parsing phase
425 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
429 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
426 safemapping = mapping.copy()
430 safemapping = mapping.copy()
427 safemapping[key] = _recursivesymbolblocker(key)
431 safemapping[key] = _recursivesymbolblocker(key)
428 try:
432 try:
429 v = context.process(key, safemapping)
433 v = context.process(key, safemapping)
430 except TemplateNotFound:
434 except TemplateNotFound:
431 v = default
435 v = default
432 if callable(v) and getattr(v, '_requires', None) is None:
436 if callable(v) and getattr(v, '_requires', None) is None:
433 # old templatekw: expand all keywords and resources
437 # old templatekw: expand all keywords and resources
434 # (TODO: deprecate this after porting web template keywords to new API)
438 # (TODO: deprecate this after porting web template keywords to new API)
435 props = {k: context._resources.lookup(context, mapping, k)
439 props = {k: context._resources.lookup(context, mapping, k)
436 for k in context._resources.knownkeys()}
440 for k in context._resources.knownkeys()}
437 # pass context to _showcompatlist() through templatekw._showlist()
441 # pass context to _showcompatlist() through templatekw._showlist()
438 props['templ'] = context
442 props['templ'] = context
439 props.update(mapping)
443 props.update(mapping)
440 return v(**pycompat.strkwargs(props))
444 return v(**pycompat.strkwargs(props))
441 if callable(v):
445 if callable(v):
442 # new templatekw
446 # new templatekw
443 try:
447 try:
444 return v(context, mapping)
448 return v(context, mapping)
445 except ResourceUnavailable:
449 except ResourceUnavailable:
446 # unsupported keyword is mapped to empty just like unknown keyword
450 # unsupported keyword is mapped to empty just like unknown keyword
447 return None
451 return None
448 return v
452 return v
449
453
450 def runtemplate(context, mapping, template):
454 def runtemplate(context, mapping, template):
451 for arg in template:
455 for arg in template:
452 yield evalrawexp(context, mapping, arg)
456 yield evalrawexp(context, mapping, arg)
453
457
454 def runfilter(context, mapping, data):
458 def runfilter(context, mapping, data):
455 arg, filt = data
459 arg, filt = data
456 thing = evalrawexp(context, mapping, arg)
460 thing = evalrawexp(context, mapping, arg)
457 intype = getattr(filt, '_intype', None)
461 intype = getattr(filt, '_intype', None)
458 try:
462 try:
459 thing = unwrapastype(context, mapping, thing, intype)
463 thing = unwrapastype(context, mapping, thing, intype)
460 return filt(thing)
464 return filt(thing)
461 except error.ParseError as e:
465 except error.ParseError as e:
462 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
466 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
463
467
464 def _formatfiltererror(arg, filt):
468 def _formatfiltererror(arg, filt):
465 fn = pycompat.sysbytes(filt.__name__)
469 fn = pycompat.sysbytes(filt.__name__)
466 sym = findsymbolicname(arg)
470 sym = findsymbolicname(arg)
467 if not sym:
471 if not sym:
468 return _("incompatible use of template filter '%s'") % fn
472 return _("incompatible use of template filter '%s'") % fn
469 return (_("template filter '%s' is not compatible with keyword '%s'")
473 return (_("template filter '%s' is not compatible with keyword '%s'")
470 % (fn, sym))
474 % (fn, sym))
471
475
472 def runmap(context, mapping, data):
476 def runmap(context, mapping, data):
473 darg, targ = data
477 darg, targ = data
474 d = evalrawexp(context, mapping, darg)
478 d = evalrawexp(context, mapping, darg)
475 if util.safehasattr(d, 'itermaps'):
479 if util.safehasattr(d, 'itermaps'):
476 diter = d.itermaps()
480 diter = d.itermaps()
477 else:
481 else:
478 try:
482 try:
479 diter = iter(d)
483 diter = iter(d)
480 except TypeError:
484 except TypeError:
481 sym = findsymbolicname(darg)
485 sym = findsymbolicname(darg)
482 if sym:
486 if sym:
483 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
487 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
484 else:
488 else:
485 raise error.ParseError(_("%r is not iterable") % d)
489 raise error.ParseError(_("%r is not iterable") % d)
486
490
487 for i, v in enumerate(diter):
491 for i, v in enumerate(diter):
488 if isinstance(v, dict):
492 if isinstance(v, dict):
489 lm = context.overlaymap(mapping, v)
493 lm = context.overlaymap(mapping, v)
490 lm['index'] = i
494 lm['index'] = i
491 yield evalrawexp(context, lm, targ)
495 yield evalrawexp(context, lm, targ)
492 else:
496 else:
493 # v is not an iterable of dicts, this happen when 'key'
497 # v is not an iterable of dicts, this happen when 'key'
494 # has been fully expanded already and format is useless.
498 # has been fully expanded already and format is useless.
495 # If so, return the expanded value.
499 # If so, return the expanded value.
496 yield v
500 yield v
497
501
498 def runmember(context, mapping, data):
502 def runmember(context, mapping, data):
499 darg, memb = data
503 darg, memb = data
500 d = evalrawexp(context, mapping, darg)
504 d = evalrawexp(context, mapping, darg)
501 if util.safehasattr(d, 'tomap'):
505 if util.safehasattr(d, 'tomap'):
502 lm = context.overlaymap(mapping, d.tomap())
506 lm = context.overlaymap(mapping, d.tomap())
503 return runsymbol(context, lm, memb)
507 return runsymbol(context, lm, memb)
504 if util.safehasattr(d, 'get'):
508 if util.safehasattr(d, 'get'):
505 return getdictitem(d, memb)
509 return getdictitem(d, memb)
506
510
507 sym = findsymbolicname(darg)
511 sym = findsymbolicname(darg)
508 if sym:
512 if sym:
509 raise error.ParseError(_("keyword '%s' has no member") % sym)
513 raise error.ParseError(_("keyword '%s' has no member") % sym)
510 else:
514 else:
511 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
515 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
512
516
513 def runnegate(context, mapping, data):
517 def runnegate(context, mapping, data):
514 data = evalinteger(context, mapping, data,
518 data = evalinteger(context, mapping, data,
515 _('negation needs an integer argument'))
519 _('negation needs an integer argument'))
516 return -data
520 return -data
517
521
518 def runarithmetic(context, mapping, data):
522 def runarithmetic(context, mapping, data):
519 func, left, right = data
523 func, left, right = data
520 left = evalinteger(context, mapping, left,
524 left = evalinteger(context, mapping, left,
521 _('arithmetic only defined on integers'))
525 _('arithmetic only defined on integers'))
522 right = evalinteger(context, mapping, right,
526 right = evalinteger(context, mapping, right,
523 _('arithmetic only defined on integers'))
527 _('arithmetic only defined on integers'))
524 try:
528 try:
525 return func(left, right)
529 return func(left, right)
526 except ZeroDivisionError:
530 except ZeroDivisionError:
527 raise error.Abort(_('division by zero is not defined'))
531 raise error.Abort(_('division by zero is not defined'))
528
532
529 def getdictitem(dictarg, key):
533 def getdictitem(dictarg, key):
530 val = dictarg.get(key)
534 val = dictarg.get(key)
531 if val is None:
535 if val is None:
532 return
536 return
533 return wraphybridvalue(dictarg, key, val)
537 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now