##// END OF EJS Templates
templater: add try-except stub to runmember()...
Yuya Nishihara -
r38259:ad06a426 default
parent child Browse files
Show More
@@ -1,708 +1,710 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 join(self, context, mapping, sep):
45 def join(self, context, mapping, sep):
46 """Join items with the separator; Returns a bytes or (possibly nested)
46 """Join items with the separator; Returns a bytes or (possibly nested)
47 generator of bytes
47 generator of bytes
48
48
49 A pre-configured template may be rendered per item if this container
49 A pre-configured template may be rendered per item if this container
50 holds unprintable items.
50 holds unprintable items.
51 """
51 """
52
52
53 @abc.abstractmethod
53 @abc.abstractmethod
54 def show(self, context, mapping):
54 def show(self, context, mapping):
55 """Return a bytes or (possibly nested) generator of bytes representing
55 """Return a bytes or (possibly nested) generator of bytes representing
56 the underlying object
56 the underlying object
57
57
58 A pre-configured template may be rendered if the underlying object is
58 A pre-configured template may be rendered if the underlying object is
59 not printable.
59 not printable.
60 """
60 """
61
61
62 @abc.abstractmethod
62 @abc.abstractmethod
63 def tovalue(self, context, mapping):
63 def tovalue(self, context, mapping):
64 """Move the inner value object out or create a value representation
64 """Move the inner value object out or create a value representation
65
65
66 A returned value must be serializable by templaterfilters.json().
66 A returned value must be serializable by templaterfilters.json().
67 """
67 """
68
68
69 class wrappedbytes(wrapped):
69 class wrappedbytes(wrapped):
70 """Wrapper for byte string"""
70 """Wrapper for byte string"""
71
71
72 def __init__(self, value):
72 def __init__(self, value):
73 self._value = value
73 self._value = value
74
74
75 def itermaps(self, context):
75 def itermaps(self, context):
76 raise error.ParseError(_('%r is not iterable of mappings')
76 raise error.ParseError(_('%r is not iterable of mappings')
77 % pycompat.bytestr(self._value))
77 % pycompat.bytestr(self._value))
78
78
79 def join(self, context, mapping, sep):
79 def join(self, context, mapping, sep):
80 return joinitems(pycompat.iterbytestr(self._value), sep)
80 return joinitems(pycompat.iterbytestr(self._value), sep)
81
81
82 def show(self, context, mapping):
82 def show(self, context, mapping):
83 return self._value
83 return self._value
84
84
85 def tovalue(self, context, mapping):
85 def tovalue(self, context, mapping):
86 return self._value
86 return self._value
87
87
88 class wrappedvalue(wrapped):
88 class wrappedvalue(wrapped):
89 """Generic wrapper for pure non-list/dict/bytes value"""
89 """Generic wrapper for pure non-list/dict/bytes value"""
90
90
91 def __init__(self, value):
91 def __init__(self, value):
92 self._value = value
92 self._value = value
93
93
94 def itermaps(self, context):
94 def itermaps(self, context):
95 raise error.ParseError(_('%r is not iterable of mappings')
95 raise error.ParseError(_('%r is not iterable of mappings')
96 % self._value)
96 % self._value)
97
97
98 def join(self, context, mapping, sep):
98 def join(self, context, mapping, sep):
99 raise error.ParseError(_('%r is not iterable') % self._value)
99 raise error.ParseError(_('%r is not iterable') % self._value)
100
100
101 def show(self, context, mapping):
101 def show(self, context, mapping):
102 return pycompat.bytestr(self._value)
102 return pycompat.bytestr(self._value)
103
103
104 def tovalue(self, context, mapping):
104 def tovalue(self, context, mapping):
105 return self._value
105 return self._value
106
106
107 # stub for representing a date type; may be a real date type that can
107 # stub for representing a date type; may be a real date type that can
108 # provide a readable string value
108 # provide a readable string value
109 class date(object):
109 class date(object):
110 pass
110 pass
111
111
112 class hybrid(wrapped):
112 class hybrid(wrapped):
113 """Wrapper for list or dict to support legacy template
113 """Wrapper for list or dict to support legacy template
114
114
115 This class allows us to handle both:
115 This class allows us to handle both:
116 - "{files}" (legacy command-line-specific list hack) and
116 - "{files}" (legacy command-line-specific list hack) and
117 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
117 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
118 and to access raw values:
118 and to access raw values:
119 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
119 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
120 - "{get(extras, key)}"
120 - "{get(extras, key)}"
121 - "{files|json}"
121 - "{files|json}"
122 """
122 """
123
123
124 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
124 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
125 self._gen = gen # generator or function returning generator
125 self._gen = gen # generator or function returning generator
126 self._values = values
126 self._values = values
127 self._makemap = makemap
127 self._makemap = makemap
128 self._joinfmt = joinfmt
128 self._joinfmt = joinfmt
129 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
129 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
130
130
131 def itermaps(self, context):
131 def itermaps(self, context):
132 makemap = self._makemap
132 makemap = self._makemap
133 for x in self._values:
133 for x in self._values:
134 yield makemap(x)
134 yield makemap(x)
135
135
136 def join(self, context, mapping, sep):
136 def join(self, context, mapping, sep):
137 # TODO: switch gen to (context, mapping) API?
137 # TODO: switch gen to (context, mapping) API?
138 return joinitems((self._joinfmt(x) for x in self._values), sep)
138 return joinitems((self._joinfmt(x) for x in self._values), sep)
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 self.join(context, mapping, ' ')
144 return self.join(context, mapping, ' ')
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 # TODO: return self._values and get rid of proxy methods
150 # TODO: return self._values and get rid of proxy methods
151 return self
151 return self
152
152
153 def __contains__(self, x):
153 def __contains__(self, x):
154 return x in self._values
154 return x in self._values
155 def __getitem__(self, key):
155 def __getitem__(self, key):
156 return self._values[key]
156 return self._values[key]
157 def __len__(self):
157 def __len__(self):
158 return len(self._values)
158 return len(self._values)
159 def __iter__(self):
159 def __iter__(self):
160 return iter(self._values)
160 return iter(self._values)
161 def __getattr__(self, name):
161 def __getattr__(self, name):
162 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
162 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
163 r'itervalues', r'keys', r'values'):
163 r'itervalues', r'keys', r'values'):
164 raise AttributeError(name)
164 raise AttributeError(name)
165 return getattr(self._values, name)
165 return getattr(self._values, name)
166
166
167 class mappable(wrapped):
167 class mappable(wrapped):
168 """Wrapper for non-list/dict object to support map operation
168 """Wrapper for non-list/dict object to support map operation
169
169
170 This class allows us to handle both:
170 This class allows us to handle both:
171 - "{manifest}"
171 - "{manifest}"
172 - "{manifest % '{rev}:{node}'}"
172 - "{manifest % '{rev}:{node}'}"
173 - "{manifest.rev}"
173 - "{manifest.rev}"
174
174
175 Unlike a hybrid, this does not simulate the behavior of the underling
175 Unlike a hybrid, this does not simulate the behavior of the underling
176 value.
176 value.
177 """
177 """
178
178
179 def __init__(self, gen, key, value, makemap):
179 def __init__(self, gen, key, value, makemap):
180 self._gen = gen # generator or function returning generator
180 self._gen = gen # generator or function returning generator
181 self._key = key
181 self._key = key
182 self._value = value # may be generator of strings
182 self._value = value # may be generator of strings
183 self._makemap = makemap
183 self._makemap = makemap
184
184
185 def tomap(self):
185 def tomap(self):
186 return self._makemap(self._key)
186 return self._makemap(self._key)
187
187
188 def itermaps(self, context):
188 def itermaps(self, context):
189 yield self.tomap()
189 yield self.tomap()
190
190
191 def join(self, context, mapping, sep):
191 def join(self, context, mapping, sep):
192 w = makewrapped(context, mapping, self._value)
192 w = makewrapped(context, mapping, self._value)
193 return w.join(context, mapping, sep)
193 return w.join(context, mapping, sep)
194
194
195 def show(self, context, mapping):
195 def show(self, context, mapping):
196 # TODO: switch gen to (context, mapping) API?
196 # TODO: switch gen to (context, mapping) API?
197 gen = self._gen
197 gen = self._gen
198 if gen is None:
198 if gen is None:
199 return pycompat.bytestr(self._value)
199 return pycompat.bytestr(self._value)
200 if callable(gen):
200 if callable(gen):
201 return gen()
201 return gen()
202 return gen
202 return gen
203
203
204 def tovalue(self, context, mapping):
204 def tovalue(self, context, mapping):
205 return _unthunk(context, mapping, self._value)
205 return _unthunk(context, mapping, self._value)
206
206
207 class _mappingsequence(wrapped):
207 class _mappingsequence(wrapped):
208 """Wrapper for sequence of template mappings
208 """Wrapper for sequence of template mappings
209
209
210 This represents an inner template structure (i.e. a list of dicts),
210 This represents an inner template structure (i.e. a list of dicts),
211 which can also be rendered by the specified named/literal template.
211 which can also be rendered by the specified named/literal template.
212
212
213 Template mappings may be nested.
213 Template mappings may be nested.
214 """
214 """
215
215
216 def __init__(self, name=None, tmpl=None, sep=''):
216 def __init__(self, name=None, tmpl=None, sep=''):
217 if name is not None and tmpl is not None:
217 if name is not None and tmpl is not None:
218 raise error.ProgrammingError('name and tmpl are mutually exclusive')
218 raise error.ProgrammingError('name and tmpl are mutually exclusive')
219 self._name = name
219 self._name = name
220 self._tmpl = tmpl
220 self._tmpl = tmpl
221 self._defaultsep = sep
221 self._defaultsep = sep
222
222
223 def join(self, context, mapping, sep):
223 def join(self, context, mapping, sep):
224 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
224 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
225 if self._name:
225 if self._name:
226 itemiter = (context.process(self._name, m) for m in mapsiter)
226 itemiter = (context.process(self._name, m) for m in mapsiter)
227 elif self._tmpl:
227 elif self._tmpl:
228 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
228 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
229 else:
229 else:
230 raise error.ParseError(_('not displayable without template'))
230 raise error.ParseError(_('not displayable without template'))
231 return joinitems(itemiter, sep)
231 return joinitems(itemiter, sep)
232
232
233 def show(self, context, mapping):
233 def show(self, context, mapping):
234 return self.join(context, mapping, self._defaultsep)
234 return self.join(context, mapping, self._defaultsep)
235
235
236 def tovalue(self, context, mapping):
236 def tovalue(self, context, mapping):
237 knownres = context.knownresourcekeys()
237 knownres = context.knownresourcekeys()
238 items = []
238 items = []
239 for nm in self.itermaps(context):
239 for nm in self.itermaps(context):
240 # drop internal resources (recursively) which shouldn't be displayed
240 # drop internal resources (recursively) which shouldn't be displayed
241 lm = context.overlaymap(mapping, nm)
241 lm = context.overlaymap(mapping, nm)
242 items.append({k: unwrapvalue(context, lm, v)
242 items.append({k: unwrapvalue(context, lm, v)
243 for k, v in nm.iteritems() if k not in knownres})
243 for k, v in nm.iteritems() if k not in knownres})
244 return items
244 return items
245
245
246 class mappinggenerator(_mappingsequence):
246 class mappinggenerator(_mappingsequence):
247 """Wrapper for generator of template mappings
247 """Wrapper for generator of template mappings
248
248
249 The function ``make(context, *args)`` should return a generator of
249 The function ``make(context, *args)`` should return a generator of
250 mapping dicts.
250 mapping dicts.
251 """
251 """
252
252
253 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
253 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
254 super(mappinggenerator, self).__init__(name, tmpl, sep)
254 super(mappinggenerator, self).__init__(name, tmpl, sep)
255 self._make = make
255 self._make = make
256 self._args = args
256 self._args = args
257
257
258 def itermaps(self, context):
258 def itermaps(self, context):
259 return self._make(context, *self._args)
259 return self._make(context, *self._args)
260
260
261 class mappinglist(_mappingsequence):
261 class mappinglist(_mappingsequence):
262 """Wrapper for list of template mappings"""
262 """Wrapper for list of template mappings"""
263
263
264 def __init__(self, mappings, name=None, tmpl=None, sep=''):
264 def __init__(self, mappings, name=None, tmpl=None, sep=''):
265 super(mappinglist, self).__init__(name, tmpl, sep)
265 super(mappinglist, self).__init__(name, tmpl, sep)
266 self._mappings = mappings
266 self._mappings = mappings
267
267
268 def itermaps(self, context):
268 def itermaps(self, context):
269 return iter(self._mappings)
269 return iter(self._mappings)
270
270
271 class mappedgenerator(wrapped):
271 class mappedgenerator(wrapped):
272 """Wrapper for generator of strings which acts as a list
272 """Wrapper for generator of strings which acts as a list
273
273
274 The function ``make(context, *args)`` should return a generator of
274 The function ``make(context, *args)`` should return a generator of
275 byte strings, or a generator of (possibly nested) generators of byte
275 byte strings, or a generator of (possibly nested) generators of byte
276 strings (i.e. a generator for a list of byte strings.)
276 strings (i.e. a generator for a list of byte strings.)
277 """
277 """
278
278
279 def __init__(self, make, args=()):
279 def __init__(self, make, args=()):
280 self._make = make
280 self._make = make
281 self._args = args
281 self._args = args
282
282
283 def _gen(self, context):
283 def _gen(self, context):
284 return self._make(context, *self._args)
284 return self._make(context, *self._args)
285
285
286 def itermaps(self, context):
286 def itermaps(self, context):
287 raise error.ParseError(_('list of strings is not mappable'))
287 raise error.ParseError(_('list of strings is not mappable'))
288
288
289 def join(self, context, mapping, sep):
289 def join(self, context, mapping, sep):
290 return joinitems(self._gen(context), sep)
290 return joinitems(self._gen(context), sep)
291
291
292 def show(self, context, mapping):
292 def show(self, context, mapping):
293 return self.join(context, mapping, '')
293 return self.join(context, mapping, '')
294
294
295 def tovalue(self, context, mapping):
295 def tovalue(self, context, mapping):
296 return [stringify(context, mapping, x) for x in self._gen(context)]
296 return [stringify(context, mapping, x) for x in self._gen(context)]
297
297
298 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
298 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
299 """Wrap data to support both dict-like and string-like operations"""
299 """Wrap data to support both dict-like and string-like operations"""
300 prefmt = pycompat.identity
300 prefmt = pycompat.identity
301 if fmt is None:
301 if fmt is None:
302 fmt = '%s=%s'
302 fmt = '%s=%s'
303 prefmt = pycompat.bytestr
303 prefmt = pycompat.bytestr
304 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
304 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
305 lambda k: fmt % (prefmt(k), prefmt(data[k])))
305 lambda k: fmt % (prefmt(k), prefmt(data[k])))
306
306
307 def hybridlist(data, name, fmt=None, gen=None):
307 def hybridlist(data, name, fmt=None, gen=None):
308 """Wrap data to support both list-like and string-like operations"""
308 """Wrap data to support both list-like and string-like operations"""
309 prefmt = pycompat.identity
309 prefmt = pycompat.identity
310 if fmt is None:
310 if fmt is None:
311 fmt = '%s'
311 fmt = '%s'
312 prefmt = pycompat.bytestr
312 prefmt = pycompat.bytestr
313 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
313 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
314
314
315 def unwraphybrid(context, mapping, thing):
315 def unwraphybrid(context, mapping, thing):
316 """Return an object which can be stringified possibly by using a legacy
316 """Return an object which can be stringified possibly by using a legacy
317 template"""
317 template"""
318 if not isinstance(thing, wrapped):
318 if not isinstance(thing, wrapped):
319 return thing
319 return thing
320 return thing.show(context, mapping)
320 return thing.show(context, mapping)
321
321
322 def wraphybridvalue(container, key, value):
322 def wraphybridvalue(container, key, value):
323 """Wrap an element of hybrid container to be mappable
323 """Wrap an element of hybrid container to be mappable
324
324
325 The key is passed to the makemap function of the given container, which
325 The key is passed to the makemap function of the given container, which
326 should be an item generated by iter(container).
326 should be an item generated by iter(container).
327 """
327 """
328 makemap = getattr(container, '_makemap', None)
328 makemap = getattr(container, '_makemap', None)
329 if makemap is None:
329 if makemap is None:
330 return value
330 return value
331 if util.safehasattr(value, '_makemap'):
331 if util.safehasattr(value, '_makemap'):
332 # a nested hybrid list/dict, which has its own way of map operation
332 # a nested hybrid list/dict, which has its own way of map operation
333 return value
333 return value
334 return mappable(None, key, value, makemap)
334 return mappable(None, key, value, makemap)
335
335
336 def compatdict(context, mapping, name, data, key='key', value='value',
336 def compatdict(context, mapping, name, data, key='key', value='value',
337 fmt=None, plural=None, separator=' '):
337 fmt=None, plural=None, separator=' '):
338 """Wrap data like hybriddict(), but also supports old-style list template
338 """Wrap data like hybriddict(), but also supports old-style list template
339
339
340 This exists for backward compatibility with the old-style template. Use
340 This exists for backward compatibility with the old-style template. Use
341 hybriddict() for new template keywords.
341 hybriddict() for new template keywords.
342 """
342 """
343 c = [{key: k, value: v} for k, v in data.iteritems()]
343 c = [{key: k, value: v} for k, v in data.iteritems()]
344 f = _showcompatlist(context, mapping, name, c, plural, separator)
344 f = _showcompatlist(context, mapping, name, c, plural, separator)
345 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
345 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
346
346
347 def compatlist(context, mapping, name, data, element=None, fmt=None,
347 def compatlist(context, mapping, name, data, element=None, fmt=None,
348 plural=None, separator=' '):
348 plural=None, separator=' '):
349 """Wrap data like hybridlist(), but also supports old-style list template
349 """Wrap data like hybridlist(), but also supports old-style list template
350
350
351 This exists for backward compatibility with the old-style template. Use
351 This exists for backward compatibility with the old-style template. Use
352 hybridlist() for new template keywords.
352 hybridlist() for new template keywords.
353 """
353 """
354 f = _showcompatlist(context, mapping, name, data, plural, separator)
354 f = _showcompatlist(context, mapping, name, data, plural, separator)
355 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
355 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
356
356
357 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
357 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
358 """Return a generator that renders old-style list template
358 """Return a generator that renders old-style list template
359
359
360 name is name of key in template map.
360 name is name of key in template map.
361 values is list of strings or dicts.
361 values is list of strings or dicts.
362 plural is plural of name, if not simply name + 's'.
362 plural is plural of name, if not simply name + 's'.
363 separator is used to join values as a string
363 separator is used to join values as a string
364
364
365 expansion works like this, given name 'foo'.
365 expansion works like this, given name 'foo'.
366
366
367 if values is empty, expand 'no_foos'.
367 if values is empty, expand 'no_foos'.
368
368
369 if 'foo' not in template map, return values as a string,
369 if 'foo' not in template map, return values as a string,
370 joined by 'separator'.
370 joined by 'separator'.
371
371
372 expand 'start_foos'.
372 expand 'start_foos'.
373
373
374 for each value, expand 'foo'. if 'last_foo' in template
374 for each value, expand 'foo'. if 'last_foo' in template
375 map, expand it instead of 'foo' for last key.
375 map, expand it instead of 'foo' for last key.
376
376
377 expand 'end_foos'.
377 expand 'end_foos'.
378 """
378 """
379 if not plural:
379 if not plural:
380 plural = name + 's'
380 plural = name + 's'
381 if not values:
381 if not values:
382 noname = 'no_' + plural
382 noname = 'no_' + plural
383 if context.preload(noname):
383 if context.preload(noname):
384 yield context.process(noname, mapping)
384 yield context.process(noname, mapping)
385 return
385 return
386 if not context.preload(name):
386 if not context.preload(name):
387 if isinstance(values[0], bytes):
387 if isinstance(values[0], bytes):
388 yield separator.join(values)
388 yield separator.join(values)
389 else:
389 else:
390 for v in values:
390 for v in values:
391 r = dict(v)
391 r = dict(v)
392 r.update(mapping)
392 r.update(mapping)
393 yield r
393 yield r
394 return
394 return
395 startname = 'start_' + plural
395 startname = 'start_' + plural
396 if context.preload(startname):
396 if context.preload(startname):
397 yield context.process(startname, mapping)
397 yield context.process(startname, mapping)
398 def one(v, tag=name):
398 def one(v, tag=name):
399 vmapping = {}
399 vmapping = {}
400 try:
400 try:
401 vmapping.update(v)
401 vmapping.update(v)
402 # Python 2 raises ValueError if the type of v is wrong. Python
402 # Python 2 raises ValueError if the type of v is wrong. Python
403 # 3 raises TypeError.
403 # 3 raises TypeError.
404 except (AttributeError, TypeError, ValueError):
404 except (AttributeError, TypeError, ValueError):
405 try:
405 try:
406 # Python 2 raises ValueError trying to destructure an e.g.
406 # Python 2 raises ValueError trying to destructure an e.g.
407 # bytes. Python 3 raises TypeError.
407 # bytes. Python 3 raises TypeError.
408 for a, b in v:
408 for a, b in v:
409 vmapping[a] = b
409 vmapping[a] = b
410 except (TypeError, ValueError):
410 except (TypeError, ValueError):
411 vmapping[name] = v
411 vmapping[name] = v
412 vmapping = context.overlaymap(mapping, vmapping)
412 vmapping = context.overlaymap(mapping, vmapping)
413 return context.process(tag, vmapping)
413 return context.process(tag, vmapping)
414 lastname = 'last_' + name
414 lastname = 'last_' + name
415 if context.preload(lastname):
415 if context.preload(lastname):
416 last = values.pop()
416 last = values.pop()
417 else:
417 else:
418 last = None
418 last = None
419 for v in values:
419 for v in values:
420 yield one(v)
420 yield one(v)
421 if last is not None:
421 if last is not None:
422 yield one(last, tag=lastname)
422 yield one(last, tag=lastname)
423 endname = 'end_' + plural
423 endname = 'end_' + plural
424 if context.preload(endname):
424 if context.preload(endname):
425 yield context.process(endname, mapping)
425 yield context.process(endname, mapping)
426
426
427 def flatten(context, mapping, thing):
427 def flatten(context, mapping, thing):
428 """Yield a single stream from a possibly nested set of iterators"""
428 """Yield a single stream from a possibly nested set of iterators"""
429 thing = unwraphybrid(context, mapping, thing)
429 thing = unwraphybrid(context, mapping, thing)
430 if isinstance(thing, bytes):
430 if isinstance(thing, bytes):
431 yield thing
431 yield thing
432 elif isinstance(thing, str):
432 elif isinstance(thing, str):
433 # We can only hit this on Python 3, and it's here to guard
433 # We can only hit this on Python 3, and it's here to guard
434 # against infinite recursion.
434 # against infinite recursion.
435 raise error.ProgrammingError('Mercurial IO including templates is done'
435 raise error.ProgrammingError('Mercurial IO including templates is done'
436 ' with bytes, not strings, got %r' % thing)
436 ' with bytes, not strings, got %r' % thing)
437 elif thing is None:
437 elif thing is None:
438 pass
438 pass
439 elif not util.safehasattr(thing, '__iter__'):
439 elif not util.safehasattr(thing, '__iter__'):
440 yield pycompat.bytestr(thing)
440 yield pycompat.bytestr(thing)
441 else:
441 else:
442 for i in thing:
442 for i in thing:
443 i = unwraphybrid(context, mapping, i)
443 i = unwraphybrid(context, mapping, i)
444 if isinstance(i, bytes):
444 if isinstance(i, bytes):
445 yield i
445 yield i
446 elif i is None:
446 elif i is None:
447 pass
447 pass
448 elif not util.safehasattr(i, '__iter__'):
448 elif not util.safehasattr(i, '__iter__'):
449 yield pycompat.bytestr(i)
449 yield pycompat.bytestr(i)
450 else:
450 else:
451 for j in flatten(context, mapping, i):
451 for j in flatten(context, mapping, i):
452 yield j
452 yield j
453
453
454 def stringify(context, mapping, thing):
454 def stringify(context, mapping, thing):
455 """Turn values into bytes by converting into text and concatenating them"""
455 """Turn values into bytes by converting into text and concatenating them"""
456 if isinstance(thing, bytes):
456 if isinstance(thing, bytes):
457 return thing # retain localstr to be round-tripped
457 return thing # retain localstr to be round-tripped
458 return b''.join(flatten(context, mapping, thing))
458 return b''.join(flatten(context, mapping, thing))
459
459
460 def findsymbolicname(arg):
460 def findsymbolicname(arg):
461 """Find symbolic name for the given compiled expression; returns None
461 """Find symbolic name for the given compiled expression; returns None
462 if nothing found reliably"""
462 if nothing found reliably"""
463 while True:
463 while True:
464 func, data = arg
464 func, data = arg
465 if func is runsymbol:
465 if func is runsymbol:
466 return data
466 return data
467 elif func is runfilter:
467 elif func is runfilter:
468 arg = data[0]
468 arg = data[0]
469 else:
469 else:
470 return None
470 return None
471
471
472 def _unthunk(context, mapping, thing):
472 def _unthunk(context, mapping, thing):
473 """Evaluate a lazy byte string into value"""
473 """Evaluate a lazy byte string into value"""
474 if not isinstance(thing, types.GeneratorType):
474 if not isinstance(thing, types.GeneratorType):
475 return thing
475 return thing
476 return stringify(context, mapping, thing)
476 return stringify(context, mapping, thing)
477
477
478 def evalrawexp(context, mapping, arg):
478 def evalrawexp(context, mapping, arg):
479 """Evaluate given argument as a bare template object which may require
479 """Evaluate given argument as a bare template object which may require
480 further processing (such as folding generator of strings)"""
480 further processing (such as folding generator of strings)"""
481 func, data = arg
481 func, data = arg
482 return func(context, mapping, data)
482 return func(context, mapping, data)
483
483
484 def evalwrapped(context, mapping, arg):
484 def evalwrapped(context, mapping, arg):
485 """Evaluate given argument to wrapped object"""
485 """Evaluate given argument to wrapped object"""
486 thing = evalrawexp(context, mapping, arg)
486 thing = evalrawexp(context, mapping, arg)
487 return makewrapped(context, mapping, thing)
487 return makewrapped(context, mapping, thing)
488
488
489 def makewrapped(context, mapping, thing):
489 def makewrapped(context, mapping, thing):
490 """Lift object to a wrapped type"""
490 """Lift object to a wrapped type"""
491 if isinstance(thing, wrapped):
491 if isinstance(thing, wrapped):
492 return thing
492 return thing
493 thing = _unthunk(context, mapping, thing)
493 thing = _unthunk(context, mapping, thing)
494 if isinstance(thing, bytes):
494 if isinstance(thing, bytes):
495 return wrappedbytes(thing)
495 return wrappedbytes(thing)
496 return wrappedvalue(thing)
496 return wrappedvalue(thing)
497
497
498 def evalfuncarg(context, mapping, arg):
498 def evalfuncarg(context, mapping, arg):
499 """Evaluate given argument as value type"""
499 """Evaluate given argument as value type"""
500 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
500 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
501
501
502 def unwrapvalue(context, mapping, thing):
502 def unwrapvalue(context, mapping, thing):
503 """Move the inner value object out of the wrapper"""
503 """Move the inner value object out of the wrapper"""
504 if isinstance(thing, wrapped):
504 if isinstance(thing, wrapped):
505 return thing.tovalue(context, mapping)
505 return thing.tovalue(context, mapping)
506 # evalrawexp() may return string, generator of strings or arbitrary object
506 # evalrawexp() may return string, generator of strings or arbitrary object
507 # such as date tuple, but filter does not want generator.
507 # such as date tuple, but filter does not want generator.
508 return _unthunk(context, mapping, thing)
508 return _unthunk(context, mapping, thing)
509
509
510 def evalboolean(context, mapping, arg):
510 def evalboolean(context, mapping, arg):
511 """Evaluate given argument as boolean, but also takes boolean literals"""
511 """Evaluate given argument as boolean, but also takes boolean literals"""
512 func, data = arg
512 func, data = arg
513 if func is runsymbol:
513 if func is runsymbol:
514 thing = func(context, mapping, data, default=None)
514 thing = func(context, mapping, data, default=None)
515 if thing is None:
515 if thing is None:
516 # not a template keyword, takes as a boolean literal
516 # not a template keyword, takes as a boolean literal
517 thing = stringutil.parsebool(data)
517 thing = stringutil.parsebool(data)
518 else:
518 else:
519 thing = func(context, mapping, data)
519 thing = func(context, mapping, data)
520 if isinstance(thing, wrapped):
520 if isinstance(thing, wrapped):
521 thing = thing.tovalue(context, mapping)
521 thing = thing.tovalue(context, mapping)
522 if isinstance(thing, bool):
522 if isinstance(thing, bool):
523 return thing
523 return thing
524 # other objects are evaluated as strings, which means 0 is True, but
524 # other objects are evaluated as strings, which means 0 is True, but
525 # empty dict/list should be False as they are expected to be ''
525 # empty dict/list should be False as they are expected to be ''
526 return bool(stringify(context, mapping, thing))
526 return bool(stringify(context, mapping, thing))
527
527
528 def evaldate(context, mapping, arg, err=None):
528 def evaldate(context, mapping, arg, err=None):
529 """Evaluate given argument as a date tuple or a date string; returns
529 """Evaluate given argument as a date tuple or a date string; returns
530 a (unixtime, offset) tuple"""
530 a (unixtime, offset) tuple"""
531 thing = evalrawexp(context, mapping, arg)
531 thing = evalrawexp(context, mapping, arg)
532 return unwrapdate(context, mapping, thing, err)
532 return unwrapdate(context, mapping, thing, err)
533
533
534 def unwrapdate(context, mapping, thing, err=None):
534 def unwrapdate(context, mapping, thing, err=None):
535 thing = unwrapvalue(context, mapping, thing)
535 thing = unwrapvalue(context, mapping, thing)
536 try:
536 try:
537 return dateutil.parsedate(thing)
537 return dateutil.parsedate(thing)
538 except AttributeError:
538 except AttributeError:
539 raise error.ParseError(err or _('not a date tuple nor a string'))
539 raise error.ParseError(err or _('not a date tuple nor a string'))
540 except error.ParseError:
540 except error.ParseError:
541 if not err:
541 if not err:
542 raise
542 raise
543 raise error.ParseError(err)
543 raise error.ParseError(err)
544
544
545 def evalinteger(context, mapping, arg, err=None):
545 def evalinteger(context, mapping, arg, err=None):
546 thing = evalrawexp(context, mapping, arg)
546 thing = evalrawexp(context, mapping, arg)
547 return unwrapinteger(context, mapping, thing, err)
547 return unwrapinteger(context, mapping, thing, err)
548
548
549 def unwrapinteger(context, mapping, thing, err=None):
549 def unwrapinteger(context, mapping, thing, err=None):
550 thing = unwrapvalue(context, mapping, thing)
550 thing = unwrapvalue(context, mapping, thing)
551 try:
551 try:
552 return int(thing)
552 return int(thing)
553 except (TypeError, ValueError):
553 except (TypeError, ValueError):
554 raise error.ParseError(err or _('not an integer'))
554 raise error.ParseError(err or _('not an integer'))
555
555
556 def evalstring(context, mapping, arg):
556 def evalstring(context, mapping, arg):
557 return stringify(context, mapping, evalrawexp(context, mapping, arg))
557 return stringify(context, mapping, evalrawexp(context, mapping, arg))
558
558
559 def evalstringliteral(context, mapping, arg):
559 def evalstringliteral(context, mapping, arg):
560 """Evaluate given argument as string template, but returns symbol name
560 """Evaluate given argument as string template, but returns symbol name
561 if it is unknown"""
561 if it is unknown"""
562 func, data = arg
562 func, data = arg
563 if func is runsymbol:
563 if func is runsymbol:
564 thing = func(context, mapping, data, default=data)
564 thing = func(context, mapping, data, default=data)
565 else:
565 else:
566 thing = func(context, mapping, data)
566 thing = func(context, mapping, data)
567 return stringify(context, mapping, thing)
567 return stringify(context, mapping, thing)
568
568
569 _unwrapfuncbytype = {
569 _unwrapfuncbytype = {
570 None: unwrapvalue,
570 None: unwrapvalue,
571 bytes: stringify,
571 bytes: stringify,
572 date: unwrapdate,
572 date: unwrapdate,
573 int: unwrapinteger,
573 int: unwrapinteger,
574 }
574 }
575
575
576 def unwrapastype(context, mapping, thing, typ):
576 def unwrapastype(context, mapping, thing, typ):
577 """Move the inner value object out of the wrapper and coerce its type"""
577 """Move the inner value object out of the wrapper and coerce its type"""
578 try:
578 try:
579 f = _unwrapfuncbytype[typ]
579 f = _unwrapfuncbytype[typ]
580 except KeyError:
580 except KeyError:
581 raise error.ProgrammingError('invalid type specified: %r' % typ)
581 raise error.ProgrammingError('invalid type specified: %r' % typ)
582 return f(context, mapping, thing)
582 return f(context, mapping, thing)
583
583
584 def runinteger(context, mapping, data):
584 def runinteger(context, mapping, data):
585 return int(data)
585 return int(data)
586
586
587 def runstring(context, mapping, data):
587 def runstring(context, mapping, data):
588 return data
588 return data
589
589
590 def _recursivesymbolblocker(key):
590 def _recursivesymbolblocker(key):
591 def showrecursion(**args):
591 def showrecursion(**args):
592 raise error.Abort(_("recursive reference '%s' in template") % key)
592 raise error.Abort(_("recursive reference '%s' in template") % key)
593 return showrecursion
593 return showrecursion
594
594
595 def runsymbol(context, mapping, key, default=''):
595 def runsymbol(context, mapping, key, default=''):
596 v = context.symbol(mapping, key)
596 v = context.symbol(mapping, key)
597 if v is None:
597 if v is None:
598 # put poison to cut recursion. we can't move this to parsing phase
598 # put poison to cut recursion. we can't move this to parsing phase
599 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
599 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
600 safemapping = mapping.copy()
600 safemapping = mapping.copy()
601 safemapping[key] = _recursivesymbolblocker(key)
601 safemapping[key] = _recursivesymbolblocker(key)
602 try:
602 try:
603 v = context.process(key, safemapping)
603 v = context.process(key, safemapping)
604 except TemplateNotFound:
604 except TemplateNotFound:
605 v = default
605 v = default
606 if callable(v) and getattr(v, '_requires', None) is None:
606 if callable(v) and getattr(v, '_requires', None) is None:
607 # old templatekw: expand all keywords and resources
607 # old templatekw: expand all keywords and resources
608 # (TODO: deprecate this after porting web template keywords to new API)
608 # (TODO: deprecate this after porting web template keywords to new API)
609 props = {k: context._resources.lookup(context, mapping, k)
609 props = {k: context._resources.lookup(context, mapping, k)
610 for k in context._resources.knownkeys()}
610 for k in context._resources.knownkeys()}
611 # pass context to _showcompatlist() through templatekw._showlist()
611 # pass context to _showcompatlist() through templatekw._showlist()
612 props['templ'] = context
612 props['templ'] = context
613 props.update(mapping)
613 props.update(mapping)
614 return v(**pycompat.strkwargs(props))
614 return v(**pycompat.strkwargs(props))
615 if callable(v):
615 if callable(v):
616 # new templatekw
616 # new templatekw
617 try:
617 try:
618 return v(context, mapping)
618 return v(context, mapping)
619 except ResourceUnavailable:
619 except ResourceUnavailable:
620 # unsupported keyword is mapped to empty just like unknown keyword
620 # unsupported keyword is mapped to empty just like unknown keyword
621 return None
621 return None
622 return v
622 return v
623
623
624 def runtemplate(context, mapping, template):
624 def runtemplate(context, mapping, template):
625 for arg in template:
625 for arg in template:
626 yield evalrawexp(context, mapping, arg)
626 yield evalrawexp(context, mapping, arg)
627
627
628 def runfilter(context, mapping, data):
628 def runfilter(context, mapping, data):
629 arg, filt = data
629 arg, filt = data
630 thing = evalrawexp(context, mapping, arg)
630 thing = evalrawexp(context, mapping, arg)
631 intype = getattr(filt, '_intype', None)
631 intype = getattr(filt, '_intype', None)
632 try:
632 try:
633 thing = unwrapastype(context, mapping, thing, intype)
633 thing = unwrapastype(context, mapping, thing, intype)
634 return filt(thing)
634 return filt(thing)
635 except error.ParseError as e:
635 except error.ParseError as e:
636 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
636 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
637
637
638 def _formatfiltererror(arg, filt):
638 def _formatfiltererror(arg, filt):
639 fn = pycompat.sysbytes(filt.__name__)
639 fn = pycompat.sysbytes(filt.__name__)
640 sym = findsymbolicname(arg)
640 sym = findsymbolicname(arg)
641 if not sym:
641 if not sym:
642 return _("incompatible use of template filter '%s'") % fn
642 return _("incompatible use of template filter '%s'") % fn
643 return (_("template filter '%s' is not compatible with keyword '%s'")
643 return (_("template filter '%s' is not compatible with keyword '%s'")
644 % (fn, sym))
644 % (fn, sym))
645
645
646 def _iteroverlaymaps(context, origmapping, newmappings):
646 def _iteroverlaymaps(context, origmapping, newmappings):
647 """Generate combined mappings from the original mapping and an iterable
647 """Generate combined mappings from the original mapping and an iterable
648 of partial mappings to override the original"""
648 of partial mappings to override the original"""
649 for i, nm in enumerate(newmappings):
649 for i, nm in enumerate(newmappings):
650 lm = context.overlaymap(origmapping, nm)
650 lm = context.overlaymap(origmapping, nm)
651 lm['index'] = i
651 lm['index'] = i
652 yield lm
652 yield lm
653
653
654 def _applymap(context, mapping, d, targ):
654 def _applymap(context, mapping, d, targ):
655 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
655 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
656 yield evalrawexp(context, lm, targ)
656 yield evalrawexp(context, lm, targ)
657
657
658 def runmap(context, mapping, data):
658 def runmap(context, mapping, data):
659 darg, targ = data
659 darg, targ = data
660 d = evalwrapped(context, mapping, darg)
660 d = evalwrapped(context, mapping, darg)
661 return mappedgenerator(_applymap, args=(mapping, d, targ))
661 return mappedgenerator(_applymap, args=(mapping, d, targ))
662
662
663 def runmember(context, mapping, data):
663 def runmember(context, mapping, data):
664 darg, memb = data
664 darg, memb = data
665 d = evalwrapped(context, mapping, darg)
665 d = evalwrapped(context, mapping, darg)
666 if util.safehasattr(d, 'tomap'):
666 if util.safehasattr(d, 'tomap'):
667 lm = context.overlaymap(mapping, d.tomap())
667 lm = context.overlaymap(mapping, d.tomap())
668 return runsymbol(context, lm, memb)
668 return runsymbol(context, lm, memb)
669 try:
669 if util.safehasattr(d, 'get'):
670 if util.safehasattr(d, 'get'):
670 return getdictitem(d, memb)
671 return getdictitem(d, memb)
671
672 raise error.ParseError
673 except error.ParseError:
672 sym = findsymbolicname(darg)
674 sym = findsymbolicname(darg)
673 if sym:
675 if sym:
674 raise error.ParseError(_("keyword '%s' has no member") % sym)
676 raise error.ParseError(_("keyword '%s' has no member") % sym)
675 else:
677 else:
676 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
678 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
677
679
678 def runnegate(context, mapping, data):
680 def runnegate(context, mapping, data):
679 data = evalinteger(context, mapping, data,
681 data = evalinteger(context, mapping, data,
680 _('negation needs an integer argument'))
682 _('negation needs an integer argument'))
681 return -data
683 return -data
682
684
683 def runarithmetic(context, mapping, data):
685 def runarithmetic(context, mapping, data):
684 func, left, right = data
686 func, left, right = data
685 left = evalinteger(context, mapping, left,
687 left = evalinteger(context, mapping, left,
686 _('arithmetic only defined on integers'))
688 _('arithmetic only defined on integers'))
687 right = evalinteger(context, mapping, right,
689 right = evalinteger(context, mapping, right,
688 _('arithmetic only defined on integers'))
690 _('arithmetic only defined on integers'))
689 try:
691 try:
690 return func(left, right)
692 return func(left, right)
691 except ZeroDivisionError:
693 except ZeroDivisionError:
692 raise error.Abort(_('division by zero is not defined'))
694 raise error.Abort(_('division by zero is not defined'))
693
695
694 def getdictitem(dictarg, key):
696 def getdictitem(dictarg, key):
695 val = dictarg.get(key)
697 val = dictarg.get(key)
696 if val is None:
698 if val is None:
697 return
699 return
698 return wraphybridvalue(dictarg, key, val)
700 return wraphybridvalue(dictarg, key, val)
699
701
700 def joinitems(itemiter, sep):
702 def joinitems(itemiter, sep):
701 """Join items with the separator; Returns generator of bytes"""
703 """Join items with the separator; Returns generator of bytes"""
702 first = True
704 first = True
703 for x in itemiter:
705 for x in itemiter:
704 if first:
706 if first:
705 first = False
707 first = False
706 elif sep:
708 elif sep:
707 yield sep
709 yield sep
708 yield x
710 yield x
General Comments 0
You need to be logged in to leave comments. Login now