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