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