##// END OF EJS Templates
templater: deduplicate iterator of overlay mappings
Yuya Nishihara -
r37423:da8e9eca default
parent child Browse files
Show More
@@ -1,652 +1,650 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 return list(self.itermaps(context))
203 return list(self.itermaps(context))
204
204
205 class mappinggenerator(_mappingsequence):
205 class mappinggenerator(_mappingsequence):
206 """Wrapper for generator of template mappings
206 """Wrapper for generator of template mappings
207
207
208 The function ``make(context, *args)`` should return a generator of
208 The function ``make(context, *args)`` should return a generator of
209 mapping dicts.
209 mapping dicts.
210 """
210 """
211
211
212 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
212 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
213 super(mappinggenerator, self).__init__(name, tmpl, sep)
213 super(mappinggenerator, self).__init__(name, tmpl, sep)
214 self._make = make
214 self._make = make
215 self._args = args
215 self._args = args
216
216
217 def itermaps(self, context):
217 def itermaps(self, context):
218 return self._make(context, *self._args)
218 return self._make(context, *self._args)
219
219
220 class mappinglist(_mappingsequence):
220 class mappinglist(_mappingsequence):
221 """Wrapper for list of template mappings"""
221 """Wrapper for list of template mappings"""
222
222
223 def __init__(self, mappings, name=None, tmpl=None, sep=''):
223 def __init__(self, mappings, name=None, tmpl=None, sep=''):
224 super(mappinglist, self).__init__(name, tmpl, sep)
224 super(mappinglist, self).__init__(name, tmpl, sep)
225 self._mappings = mappings
225 self._mappings = mappings
226
226
227 def itermaps(self, context):
227 def itermaps(self, context):
228 return iter(self._mappings)
228 return iter(self._mappings)
229
229
230 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
230 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
231 """Wrap data to support both dict-like and string-like operations"""
231 """Wrap data to support both dict-like and string-like operations"""
232 prefmt = pycompat.identity
232 prefmt = pycompat.identity
233 if fmt is None:
233 if fmt is None:
234 fmt = '%s=%s'
234 fmt = '%s=%s'
235 prefmt = pycompat.bytestr
235 prefmt = pycompat.bytestr
236 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
236 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
237 lambda k: fmt % (prefmt(k), prefmt(data[k])))
237 lambda k: fmt % (prefmt(k), prefmt(data[k])))
238
238
239 def hybridlist(data, name, fmt=None, gen=None):
239 def hybridlist(data, name, fmt=None, gen=None):
240 """Wrap data to support both list-like and string-like operations"""
240 """Wrap data to support both list-like and string-like operations"""
241 prefmt = pycompat.identity
241 prefmt = pycompat.identity
242 if fmt is None:
242 if fmt is None:
243 fmt = '%s'
243 fmt = '%s'
244 prefmt = pycompat.bytestr
244 prefmt = pycompat.bytestr
245 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
245 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
246
246
247 def unwraphybrid(context, mapping, thing):
247 def unwraphybrid(context, mapping, thing):
248 """Return an object which can be stringified possibly by using a legacy
248 """Return an object which can be stringified possibly by using a legacy
249 template"""
249 template"""
250 if not isinstance(thing, wrapped):
250 if not isinstance(thing, wrapped):
251 return thing
251 return thing
252 return thing.show(context, mapping)
252 return thing.show(context, mapping)
253
253
254 def unwrapvalue(context, mapping, thing):
254 def unwrapvalue(context, mapping, thing):
255 """Move the inner value object out of the wrapper"""
255 """Move the inner value object out of the wrapper"""
256 if not isinstance(thing, wrapped):
256 if not isinstance(thing, wrapped):
257 return thing
257 return thing
258 return thing.tovalue(context, mapping)
258 return thing.tovalue(context, mapping)
259
259
260 def wraphybridvalue(container, key, value):
260 def wraphybridvalue(container, key, value):
261 """Wrap an element of hybrid container to be mappable
261 """Wrap an element of hybrid container to be mappable
262
262
263 The key is passed to the makemap function of the given container, which
263 The key is passed to the makemap function of the given container, which
264 should be an item generated by iter(container).
264 should be an item generated by iter(container).
265 """
265 """
266 makemap = getattr(container, '_makemap', None)
266 makemap = getattr(container, '_makemap', None)
267 if makemap is None:
267 if makemap is None:
268 return value
268 return value
269 if util.safehasattr(value, '_makemap'):
269 if util.safehasattr(value, '_makemap'):
270 # a nested hybrid list/dict, which has its own way of map operation
270 # a nested hybrid list/dict, which has its own way of map operation
271 return value
271 return value
272 return mappable(None, key, value, makemap)
272 return mappable(None, key, value, makemap)
273
273
274 def compatdict(context, mapping, name, data, key='key', value='value',
274 def compatdict(context, mapping, name, data, key='key', value='value',
275 fmt=None, plural=None, separator=' '):
275 fmt=None, plural=None, separator=' '):
276 """Wrap data like hybriddict(), but also supports old-style list template
276 """Wrap data like hybriddict(), but also supports old-style list template
277
277
278 This exists for backward compatibility with the old-style template. Use
278 This exists for backward compatibility with the old-style template. Use
279 hybriddict() for new template keywords.
279 hybriddict() for new template keywords.
280 """
280 """
281 c = [{key: k, value: v} for k, v in data.iteritems()]
281 c = [{key: k, value: v} for k, v in data.iteritems()]
282 f = _showcompatlist(context, mapping, name, c, plural, separator)
282 f = _showcompatlist(context, mapping, name, c, plural, separator)
283 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
283 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
284
284
285 def compatlist(context, mapping, name, data, element=None, fmt=None,
285 def compatlist(context, mapping, name, data, element=None, fmt=None,
286 plural=None, separator=' '):
286 plural=None, separator=' '):
287 """Wrap data like hybridlist(), but also supports old-style list template
287 """Wrap data like hybridlist(), but also supports old-style list template
288
288
289 This exists for backward compatibility with the old-style template. Use
289 This exists for backward compatibility with the old-style template. Use
290 hybridlist() for new template keywords.
290 hybridlist() for new template keywords.
291 """
291 """
292 f = _showcompatlist(context, mapping, name, data, plural, separator)
292 f = _showcompatlist(context, mapping, name, data, plural, separator)
293 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
293 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
294
294
295 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
295 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
296 """Return a generator that renders old-style list template
296 """Return a generator that renders old-style list template
297
297
298 name is name of key in template map.
298 name is name of key in template map.
299 values is list of strings or dicts.
299 values is list of strings or dicts.
300 plural is plural of name, if not simply name + 's'.
300 plural is plural of name, if not simply name + 's'.
301 separator is used to join values as a string
301 separator is used to join values as a string
302
302
303 expansion works like this, given name 'foo'.
303 expansion works like this, given name 'foo'.
304
304
305 if values is empty, expand 'no_foos'.
305 if values is empty, expand 'no_foos'.
306
306
307 if 'foo' not in template map, return values as a string,
307 if 'foo' not in template map, return values as a string,
308 joined by 'separator'.
308 joined by 'separator'.
309
309
310 expand 'start_foos'.
310 expand 'start_foos'.
311
311
312 for each value, expand 'foo'. if 'last_foo' in template
312 for each value, expand 'foo'. if 'last_foo' in template
313 map, expand it instead of 'foo' for last key.
313 map, expand it instead of 'foo' for last key.
314
314
315 expand 'end_foos'.
315 expand 'end_foos'.
316 """
316 """
317 if not plural:
317 if not plural:
318 plural = name + 's'
318 plural = name + 's'
319 if not values:
319 if not values:
320 noname = 'no_' + plural
320 noname = 'no_' + plural
321 if context.preload(noname):
321 if context.preload(noname):
322 yield context.process(noname, mapping)
322 yield context.process(noname, mapping)
323 return
323 return
324 if not context.preload(name):
324 if not context.preload(name):
325 if isinstance(values[0], bytes):
325 if isinstance(values[0], bytes):
326 yield separator.join(values)
326 yield separator.join(values)
327 else:
327 else:
328 for v in values:
328 for v in values:
329 r = dict(v)
329 r = dict(v)
330 r.update(mapping)
330 r.update(mapping)
331 yield r
331 yield r
332 return
332 return
333 startname = 'start_' + plural
333 startname = 'start_' + plural
334 if context.preload(startname):
334 if context.preload(startname):
335 yield context.process(startname, mapping)
335 yield context.process(startname, mapping)
336 def one(v, tag=name):
336 def one(v, tag=name):
337 vmapping = {}
337 vmapping = {}
338 try:
338 try:
339 vmapping.update(v)
339 vmapping.update(v)
340 # Python 2 raises ValueError if the type of v is wrong. Python
340 # Python 2 raises ValueError if the type of v is wrong. Python
341 # 3 raises TypeError.
341 # 3 raises TypeError.
342 except (AttributeError, TypeError, ValueError):
342 except (AttributeError, TypeError, ValueError):
343 try:
343 try:
344 # Python 2 raises ValueError trying to destructure an e.g.
344 # Python 2 raises ValueError trying to destructure an e.g.
345 # bytes. Python 3 raises TypeError.
345 # bytes. Python 3 raises TypeError.
346 for a, b in v:
346 for a, b in v:
347 vmapping[a] = b
347 vmapping[a] = b
348 except (TypeError, ValueError):
348 except (TypeError, ValueError):
349 vmapping[name] = v
349 vmapping[name] = v
350 vmapping = context.overlaymap(mapping, vmapping)
350 vmapping = context.overlaymap(mapping, vmapping)
351 return context.process(tag, vmapping)
351 return context.process(tag, vmapping)
352 lastname = 'last_' + name
352 lastname = 'last_' + name
353 if context.preload(lastname):
353 if context.preload(lastname):
354 last = values.pop()
354 last = values.pop()
355 else:
355 else:
356 last = None
356 last = None
357 for v in values:
357 for v in values:
358 yield one(v)
358 yield one(v)
359 if last is not None:
359 if last is not None:
360 yield one(last, tag=lastname)
360 yield one(last, tag=lastname)
361 endname = 'end_' + plural
361 endname = 'end_' + plural
362 if context.preload(endname):
362 if context.preload(endname):
363 yield context.process(endname, mapping)
363 yield context.process(endname, mapping)
364
364
365 def flatten(context, mapping, thing):
365 def flatten(context, mapping, thing):
366 """Yield a single stream from a possibly nested set of iterators"""
366 """Yield a single stream from a possibly nested set of iterators"""
367 thing = unwraphybrid(context, mapping, thing)
367 thing = unwraphybrid(context, mapping, thing)
368 if isinstance(thing, bytes):
368 if isinstance(thing, bytes):
369 yield thing
369 yield thing
370 elif isinstance(thing, str):
370 elif isinstance(thing, str):
371 # We can only hit this on Python 3, and it's here to guard
371 # We can only hit this on Python 3, and it's here to guard
372 # against infinite recursion.
372 # against infinite recursion.
373 raise error.ProgrammingError('Mercurial IO including templates is done'
373 raise error.ProgrammingError('Mercurial IO including templates is done'
374 ' with bytes, not strings, got %r' % thing)
374 ' with bytes, not strings, got %r' % thing)
375 elif thing is None:
375 elif thing is None:
376 pass
376 pass
377 elif not util.safehasattr(thing, '__iter__'):
377 elif not util.safehasattr(thing, '__iter__'):
378 yield pycompat.bytestr(thing)
378 yield pycompat.bytestr(thing)
379 else:
379 else:
380 for i in thing:
380 for i in thing:
381 i = unwraphybrid(context, mapping, i)
381 i = unwraphybrid(context, mapping, i)
382 if isinstance(i, bytes):
382 if isinstance(i, bytes):
383 yield i
383 yield i
384 elif i is None:
384 elif i is None:
385 pass
385 pass
386 elif not util.safehasattr(i, '__iter__'):
386 elif not util.safehasattr(i, '__iter__'):
387 yield pycompat.bytestr(i)
387 yield pycompat.bytestr(i)
388 else:
388 else:
389 for j in flatten(context, mapping, i):
389 for j in flatten(context, mapping, i):
390 yield j
390 yield j
391
391
392 def stringify(context, mapping, thing):
392 def stringify(context, mapping, thing):
393 """Turn values into bytes by converting into text and concatenating them"""
393 """Turn values into bytes by converting into text and concatenating them"""
394 if isinstance(thing, bytes):
394 if isinstance(thing, bytes):
395 return thing # retain localstr to be round-tripped
395 return thing # retain localstr to be round-tripped
396 return b''.join(flatten(context, mapping, thing))
396 return b''.join(flatten(context, mapping, thing))
397
397
398 def findsymbolicname(arg):
398 def findsymbolicname(arg):
399 """Find symbolic name for the given compiled expression; returns None
399 """Find symbolic name for the given compiled expression; returns None
400 if nothing found reliably"""
400 if nothing found reliably"""
401 while True:
401 while True:
402 func, data = arg
402 func, data = arg
403 if func is runsymbol:
403 if func is runsymbol:
404 return data
404 return data
405 elif func is runfilter:
405 elif func is runfilter:
406 arg = data[0]
406 arg = data[0]
407 else:
407 else:
408 return None
408 return None
409
409
410 def _unthunk(context, mapping, thing):
410 def _unthunk(context, mapping, thing):
411 """Evaluate a lazy byte string into value"""
411 """Evaluate a lazy byte string into value"""
412 if not isinstance(thing, types.GeneratorType):
412 if not isinstance(thing, types.GeneratorType):
413 return thing
413 return thing
414 return stringify(context, mapping, thing)
414 return stringify(context, mapping, thing)
415
415
416 def evalrawexp(context, mapping, arg):
416 def evalrawexp(context, mapping, arg):
417 """Evaluate given argument as a bare template object which may require
417 """Evaluate given argument as a bare template object which may require
418 further processing (such as folding generator of strings)"""
418 further processing (such as folding generator of strings)"""
419 func, data = arg
419 func, data = arg
420 return func(context, mapping, data)
420 return func(context, mapping, data)
421
421
422 def evalfuncarg(context, mapping, arg):
422 def evalfuncarg(context, mapping, arg):
423 """Evaluate given argument as value type"""
423 """Evaluate given argument as value type"""
424 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
424 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
425
425
426 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
426 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
427 # is fixed. we can't do that right now because join() has to take a generator
427 # is fixed. we can't do that right now because join() has to take a generator
428 # of byte strings as it is, not a lazy byte string.
428 # of byte strings as it is, not a lazy byte string.
429 def _unwrapvalue(context, mapping, thing):
429 def _unwrapvalue(context, mapping, thing):
430 thing = unwrapvalue(context, mapping, thing)
430 thing = unwrapvalue(context, mapping, thing)
431 # evalrawexp() may return string, generator of strings or arbitrary object
431 # evalrawexp() may return string, generator of strings or arbitrary object
432 # such as date tuple, but filter does not want generator.
432 # such as date tuple, but filter does not want generator.
433 return _unthunk(context, mapping, thing)
433 return _unthunk(context, mapping, thing)
434
434
435 def evalboolean(context, mapping, arg):
435 def evalboolean(context, mapping, arg):
436 """Evaluate given argument as boolean, but also takes boolean literals"""
436 """Evaluate given argument as boolean, but also takes boolean literals"""
437 func, data = arg
437 func, data = arg
438 if func is runsymbol:
438 if func is runsymbol:
439 thing = func(context, mapping, data, default=None)
439 thing = func(context, mapping, data, default=None)
440 if thing is None:
440 if thing is None:
441 # not a template keyword, takes as a boolean literal
441 # not a template keyword, takes as a boolean literal
442 thing = stringutil.parsebool(data)
442 thing = stringutil.parsebool(data)
443 else:
443 else:
444 thing = func(context, mapping, data)
444 thing = func(context, mapping, data)
445 thing = unwrapvalue(context, mapping, thing)
445 thing = unwrapvalue(context, mapping, thing)
446 if isinstance(thing, bool):
446 if isinstance(thing, bool):
447 return thing
447 return thing
448 # other objects are evaluated as strings, which means 0 is True, but
448 # other objects are evaluated as strings, which means 0 is True, but
449 # empty dict/list should be False as they are expected to be ''
449 # empty dict/list should be False as they are expected to be ''
450 return bool(stringify(context, mapping, thing))
450 return bool(stringify(context, mapping, thing))
451
451
452 def evaldate(context, mapping, arg, err=None):
452 def evaldate(context, mapping, arg, err=None):
453 """Evaluate given argument as a date tuple or a date string; returns
453 """Evaluate given argument as a date tuple or a date string; returns
454 a (unixtime, offset) tuple"""
454 a (unixtime, offset) tuple"""
455 thing = evalrawexp(context, mapping, arg)
455 thing = evalrawexp(context, mapping, arg)
456 return unwrapdate(context, mapping, thing, err)
456 return unwrapdate(context, mapping, thing, err)
457
457
458 def unwrapdate(context, mapping, thing, err=None):
458 def unwrapdate(context, mapping, thing, err=None):
459 thing = _unwrapvalue(context, mapping, thing)
459 thing = _unwrapvalue(context, mapping, thing)
460 try:
460 try:
461 return dateutil.parsedate(thing)
461 return dateutil.parsedate(thing)
462 except AttributeError:
462 except AttributeError:
463 raise error.ParseError(err or _('not a date tuple nor a string'))
463 raise error.ParseError(err or _('not a date tuple nor a string'))
464 except error.ParseError:
464 except error.ParseError:
465 if not err:
465 if not err:
466 raise
466 raise
467 raise error.ParseError(err)
467 raise error.ParseError(err)
468
468
469 def evalinteger(context, mapping, arg, err=None):
469 def evalinteger(context, mapping, arg, err=None):
470 thing = evalrawexp(context, mapping, arg)
470 thing = evalrawexp(context, mapping, arg)
471 return unwrapinteger(context, mapping, thing, err)
471 return unwrapinteger(context, mapping, thing, err)
472
472
473 def unwrapinteger(context, mapping, thing, err=None):
473 def unwrapinteger(context, mapping, thing, err=None):
474 thing = _unwrapvalue(context, mapping, thing)
474 thing = _unwrapvalue(context, mapping, thing)
475 try:
475 try:
476 return int(thing)
476 return int(thing)
477 except (TypeError, ValueError):
477 except (TypeError, ValueError):
478 raise error.ParseError(err or _('not an integer'))
478 raise error.ParseError(err or _('not an integer'))
479
479
480 def evalstring(context, mapping, arg):
480 def evalstring(context, mapping, arg):
481 return stringify(context, mapping, evalrawexp(context, mapping, arg))
481 return stringify(context, mapping, evalrawexp(context, mapping, arg))
482
482
483 def evalstringliteral(context, mapping, arg):
483 def evalstringliteral(context, mapping, arg):
484 """Evaluate given argument as string template, but returns symbol name
484 """Evaluate given argument as string template, but returns symbol name
485 if it is unknown"""
485 if it is unknown"""
486 func, data = arg
486 func, data = arg
487 if func is runsymbol:
487 if func is runsymbol:
488 thing = func(context, mapping, data, default=data)
488 thing = func(context, mapping, data, default=data)
489 else:
489 else:
490 thing = func(context, mapping, data)
490 thing = func(context, mapping, data)
491 return stringify(context, mapping, thing)
491 return stringify(context, mapping, thing)
492
492
493 _unwrapfuncbytype = {
493 _unwrapfuncbytype = {
494 None: _unwrapvalue,
494 None: _unwrapvalue,
495 bytes: stringify,
495 bytes: stringify,
496 date: unwrapdate,
496 date: unwrapdate,
497 int: unwrapinteger,
497 int: unwrapinteger,
498 }
498 }
499
499
500 def unwrapastype(context, mapping, thing, typ):
500 def unwrapastype(context, mapping, thing, typ):
501 """Move the inner value object out of the wrapper and coerce its type"""
501 """Move the inner value object out of the wrapper and coerce its type"""
502 try:
502 try:
503 f = _unwrapfuncbytype[typ]
503 f = _unwrapfuncbytype[typ]
504 except KeyError:
504 except KeyError:
505 raise error.ProgrammingError('invalid type specified: %r' % typ)
505 raise error.ProgrammingError('invalid type specified: %r' % typ)
506 return f(context, mapping, thing)
506 return f(context, mapping, thing)
507
507
508 def runinteger(context, mapping, data):
508 def runinteger(context, mapping, data):
509 return int(data)
509 return int(data)
510
510
511 def runstring(context, mapping, data):
511 def runstring(context, mapping, data):
512 return data
512 return data
513
513
514 def _recursivesymbolblocker(key):
514 def _recursivesymbolblocker(key):
515 def showrecursion(**args):
515 def showrecursion(**args):
516 raise error.Abort(_("recursive reference '%s' in template") % key)
516 raise error.Abort(_("recursive reference '%s' in template") % key)
517 return showrecursion
517 return showrecursion
518
518
519 def runsymbol(context, mapping, key, default=''):
519 def runsymbol(context, mapping, key, default=''):
520 v = context.symbol(mapping, key)
520 v = context.symbol(mapping, key)
521 if v is None:
521 if v is None:
522 # put poison to cut recursion. we can't move this to parsing phase
522 # put poison to cut recursion. we can't move this to parsing phase
523 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
523 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
524 safemapping = mapping.copy()
524 safemapping = mapping.copy()
525 safemapping[key] = _recursivesymbolblocker(key)
525 safemapping[key] = _recursivesymbolblocker(key)
526 try:
526 try:
527 v = context.process(key, safemapping)
527 v = context.process(key, safemapping)
528 except TemplateNotFound:
528 except TemplateNotFound:
529 v = default
529 v = default
530 if callable(v) and getattr(v, '_requires', None) is None:
530 if callable(v) and getattr(v, '_requires', None) is None:
531 # old templatekw: expand all keywords and resources
531 # old templatekw: expand all keywords and resources
532 # (TODO: deprecate this after porting web template keywords to new API)
532 # (TODO: deprecate this after porting web template keywords to new API)
533 props = {k: context._resources.lookup(context, mapping, k)
533 props = {k: context._resources.lookup(context, mapping, k)
534 for k in context._resources.knownkeys()}
534 for k in context._resources.knownkeys()}
535 # pass context to _showcompatlist() through templatekw._showlist()
535 # pass context to _showcompatlist() through templatekw._showlist()
536 props['templ'] = context
536 props['templ'] = context
537 props.update(mapping)
537 props.update(mapping)
538 return v(**pycompat.strkwargs(props))
538 return v(**pycompat.strkwargs(props))
539 if callable(v):
539 if callable(v):
540 # new templatekw
540 # new templatekw
541 try:
541 try:
542 return v(context, mapping)
542 return v(context, mapping)
543 except ResourceUnavailable:
543 except ResourceUnavailable:
544 # unsupported keyword is mapped to empty just like unknown keyword
544 # unsupported keyword is mapped to empty just like unknown keyword
545 return None
545 return None
546 return v
546 return v
547
547
548 def runtemplate(context, mapping, template):
548 def runtemplate(context, mapping, template):
549 for arg in template:
549 for arg in template:
550 yield evalrawexp(context, mapping, arg)
550 yield evalrawexp(context, mapping, arg)
551
551
552 def runfilter(context, mapping, data):
552 def runfilter(context, mapping, data):
553 arg, filt = data
553 arg, filt = data
554 thing = evalrawexp(context, mapping, arg)
554 thing = evalrawexp(context, mapping, arg)
555 intype = getattr(filt, '_intype', None)
555 intype = getattr(filt, '_intype', None)
556 try:
556 try:
557 thing = unwrapastype(context, mapping, thing, intype)
557 thing = unwrapastype(context, mapping, thing, intype)
558 return filt(thing)
558 return filt(thing)
559 except error.ParseError as e:
559 except error.ParseError as e:
560 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
560 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
561
561
562 def _formatfiltererror(arg, filt):
562 def _formatfiltererror(arg, filt):
563 fn = pycompat.sysbytes(filt.__name__)
563 fn = pycompat.sysbytes(filt.__name__)
564 sym = findsymbolicname(arg)
564 sym = findsymbolicname(arg)
565 if not sym:
565 if not sym:
566 return _("incompatible use of template filter '%s'") % fn
566 return _("incompatible use of template filter '%s'") % fn
567 return (_("template filter '%s' is not compatible with keyword '%s'")
567 return (_("template filter '%s' is not compatible with keyword '%s'")
568 % (fn, sym))
568 % (fn, sym))
569
569
570 def _checkeditermaps(darg, d):
570 def _checkeditermaps(darg, d):
571 try:
571 try:
572 for v in d:
572 for v in d:
573 if not isinstance(v, dict):
573 if not isinstance(v, dict):
574 raise TypeError
574 raise TypeError
575 yield v
575 yield v
576 except TypeError:
576 except TypeError:
577 sym = findsymbolicname(darg)
577 sym = findsymbolicname(darg)
578 if sym:
578 if sym:
579 raise error.ParseError(_("keyword '%s' is not iterable of mappings")
579 raise error.ParseError(_("keyword '%s' is not iterable of mappings")
580 % sym)
580 % sym)
581 else:
581 else:
582 raise error.ParseError(_("%r is not iterable of mappings") % d)
582 raise error.ParseError(_("%r is not iterable of mappings") % d)
583
583
584 def _iteroverlaymaps(context, origmapping, newmappings):
584 def _iteroverlaymaps(context, origmapping, newmappings):
585 """Generate combined mappings from the original mapping and an iterable
585 """Generate combined mappings from the original mapping and an iterable
586 of partial mappings to override the original"""
586 of partial mappings to override the original"""
587 for i, nm in enumerate(newmappings):
587 for i, nm in enumerate(newmappings):
588 lm = context.overlaymap(origmapping, nm)
588 lm = context.overlaymap(origmapping, nm)
589 lm['index'] = i
589 lm['index'] = i
590 yield lm
590 yield lm
591
591
592 def runmap(context, mapping, data):
592 def runmap(context, mapping, data):
593 darg, targ = data
593 darg, targ = data
594 d = evalrawexp(context, mapping, darg)
594 d = evalrawexp(context, mapping, darg)
595 # TODO: a generator should be rejected because it is a thunk of lazy
595 # TODO: a generator should be rejected because it is a thunk of lazy
596 # string, but we can't because hgweb abuses generator as a keyword
596 # string, but we can't because hgweb abuses generator as a keyword
597 # that returns a list of dicts.
597 # that returns a list of dicts.
598 if isinstance(d, wrapped):
598 if isinstance(d, wrapped):
599 diter = d.itermaps(context)
599 diter = d.itermaps(context)
600 else:
600 else:
601 diter = _checkeditermaps(darg, d)
601 diter = _checkeditermaps(darg, d)
602 for i, v in enumerate(diter):
602 for lm in _iteroverlaymaps(context, mapping, diter):
603 lm = context.overlaymap(mapping, v)
604 lm['index'] = i
605 yield evalrawexp(context, lm, targ)
603 yield evalrawexp(context, lm, targ)
606
604
607 def runmember(context, mapping, data):
605 def runmember(context, mapping, data):
608 darg, memb = data
606 darg, memb = data
609 d = evalrawexp(context, mapping, darg)
607 d = evalrawexp(context, mapping, darg)
610 if util.safehasattr(d, 'tomap'):
608 if util.safehasattr(d, 'tomap'):
611 lm = context.overlaymap(mapping, d.tomap())
609 lm = context.overlaymap(mapping, d.tomap())
612 return runsymbol(context, lm, memb)
610 return runsymbol(context, lm, memb)
613 if util.safehasattr(d, 'get'):
611 if util.safehasattr(d, 'get'):
614 return getdictitem(d, memb)
612 return getdictitem(d, memb)
615
613
616 sym = findsymbolicname(darg)
614 sym = findsymbolicname(darg)
617 if sym:
615 if sym:
618 raise error.ParseError(_("keyword '%s' has no member") % sym)
616 raise error.ParseError(_("keyword '%s' has no member") % sym)
619 else:
617 else:
620 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
618 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
621
619
622 def runnegate(context, mapping, data):
620 def runnegate(context, mapping, data):
623 data = evalinteger(context, mapping, data,
621 data = evalinteger(context, mapping, data,
624 _('negation needs an integer argument'))
622 _('negation needs an integer argument'))
625 return -data
623 return -data
626
624
627 def runarithmetic(context, mapping, data):
625 def runarithmetic(context, mapping, data):
628 func, left, right = data
626 func, left, right = data
629 left = evalinteger(context, mapping, left,
627 left = evalinteger(context, mapping, left,
630 _('arithmetic only defined on integers'))
628 _('arithmetic only defined on integers'))
631 right = evalinteger(context, mapping, right,
629 right = evalinteger(context, mapping, right,
632 _('arithmetic only defined on integers'))
630 _('arithmetic only defined on integers'))
633 try:
631 try:
634 return func(left, right)
632 return func(left, right)
635 except ZeroDivisionError:
633 except ZeroDivisionError:
636 raise error.Abort(_('division by zero is not defined'))
634 raise error.Abort(_('division by zero is not defined'))
637
635
638 def getdictitem(dictarg, key):
636 def getdictitem(dictarg, key):
639 val = dictarg.get(key)
637 val = dictarg.get(key)
640 if val is None:
638 if val is None:
641 return
639 return
642 return wraphybridvalue(dictarg, key, val)
640 return wraphybridvalue(dictarg, key, val)
643
641
644 def joinitems(itemiter, sep):
642 def joinitems(itemiter, sep):
645 """Join items with the separator; Returns generator of bytes"""
643 """Join items with the separator; Returns generator of bytes"""
646 first = True
644 first = True
647 for x in itemiter:
645 for x in itemiter:
648 if first:
646 if first:
649 first = False
647 first = False
650 elif sep:
648 elif sep:
651 yield sep
649 yield sep
652 yield x
650 yield x
General Comments 0
You need to be logged in to leave comments. Login now