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