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