##// END OF EJS Templates
templateutil: reimplement stringify() using flatten()
Yuya Nishihara -
r37175:e09d2183 default
parent child Browse files
Show More
@@ -1,478 +1,469 b''
1 1 # templateutil.py - utility for template evaluation
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import types
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 )
18 18 from .utils import (
19 19 stringutil,
20 20 )
21 21
22 22 class ResourceUnavailable(error.Abort):
23 23 pass
24 24
25 25 class TemplateNotFound(error.Abort):
26 26 pass
27 27
28 28 class hybrid(object):
29 29 """Wrapper for list or dict to support legacy template
30 30
31 31 This class allows us to handle both:
32 32 - "{files}" (legacy command-line-specific list hack) and
33 33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 34 and to access raw values:
35 35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 36 - "{get(extras, key)}"
37 37 - "{files|json}"
38 38 """
39 39
40 40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 41 if gen is not None:
42 42 self.gen = gen # generator or function returning generator
43 43 self._values = values
44 44 self._makemap = makemap
45 45 self.joinfmt = joinfmt
46 46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 47 def gen(self):
48 48 """Default generator to stringify this as {join(self, ' ')}"""
49 49 for i, x in enumerate(self._values):
50 50 if i > 0:
51 51 yield ' '
52 52 yield self.joinfmt(x)
53 53 def itermaps(self):
54 54 makemap = self._makemap
55 55 for x in self._values:
56 56 yield makemap(x)
57 57 def __contains__(self, x):
58 58 return x in self._values
59 59 def __getitem__(self, key):
60 60 return self._values[key]
61 61 def __len__(self):
62 62 return len(self._values)
63 63 def __iter__(self):
64 64 return iter(self._values)
65 65 def __getattr__(self, name):
66 66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 67 r'itervalues', r'keys', r'values'):
68 68 raise AttributeError(name)
69 69 return getattr(self._values, name)
70 70
71 71 class mappable(object):
72 72 """Wrapper for non-list/dict object to support map operation
73 73
74 74 This class allows us to handle both:
75 75 - "{manifest}"
76 76 - "{manifest % '{rev}:{node}'}"
77 77 - "{manifest.rev}"
78 78
79 79 Unlike a hybrid, this does not simulate the behavior of the underling
80 80 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 81 """
82 82
83 83 def __init__(self, gen, key, value, makemap):
84 84 if gen is not None:
85 85 self.gen = gen # generator or function returning generator
86 86 self._key = key
87 87 self._value = value # may be generator of strings
88 88 self._makemap = makemap
89 89
90 90 def gen(self):
91 91 yield pycompat.bytestr(self._value)
92 92
93 93 def tomap(self):
94 94 return self._makemap(self._key)
95 95
96 96 def itermaps(self):
97 97 yield self.tomap()
98 98
99 99 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
100 100 """Wrap data to support both dict-like and string-like operations"""
101 101 prefmt = pycompat.identity
102 102 if fmt is None:
103 103 fmt = '%s=%s'
104 104 prefmt = pycompat.bytestr
105 105 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
106 106 lambda k: fmt % (prefmt(k), prefmt(data[k])))
107 107
108 108 def hybridlist(data, name, fmt=None, gen=None):
109 109 """Wrap data to support both list-like and string-like operations"""
110 110 prefmt = pycompat.identity
111 111 if fmt is None:
112 112 fmt = '%s'
113 113 prefmt = pycompat.bytestr
114 114 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
115 115
116 116 def unwraphybrid(thing):
117 117 """Return an object which can be stringified possibly by using a legacy
118 118 template"""
119 119 gen = getattr(thing, 'gen', None)
120 120 if gen is None:
121 121 return thing
122 122 if callable(gen):
123 123 return gen()
124 124 return gen
125 125
126 126 def unwrapvalue(thing):
127 127 """Move the inner value object out of the wrapper"""
128 128 if not util.safehasattr(thing, '_value'):
129 129 return thing
130 130 return thing._value
131 131
132 132 def wraphybridvalue(container, key, value):
133 133 """Wrap an element of hybrid container to be mappable
134 134
135 135 The key is passed to the makemap function of the given container, which
136 136 should be an item generated by iter(container).
137 137 """
138 138 makemap = getattr(container, '_makemap', None)
139 139 if makemap is None:
140 140 return value
141 141 if util.safehasattr(value, '_makemap'):
142 142 # a nested hybrid list/dict, which has its own way of map operation
143 143 return value
144 144 return mappable(None, key, value, makemap)
145 145
146 146 def compatdict(context, mapping, name, data, key='key', value='value',
147 147 fmt=None, plural=None, separator=' '):
148 148 """Wrap data like hybriddict(), but also supports old-style list template
149 149
150 150 This exists for backward compatibility with the old-style template. Use
151 151 hybriddict() for new template keywords.
152 152 """
153 153 c = [{key: k, value: v} for k, v in data.iteritems()]
154 154 f = _showcompatlist(context, mapping, name, c, plural, separator)
155 155 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
156 156
157 157 def compatlist(context, mapping, name, data, element=None, fmt=None,
158 158 plural=None, separator=' '):
159 159 """Wrap data like hybridlist(), but also supports old-style list template
160 160
161 161 This exists for backward compatibility with the old-style template. Use
162 162 hybridlist() for new template keywords.
163 163 """
164 164 f = _showcompatlist(context, mapping, name, data, plural, separator)
165 165 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
166 166
167 167 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
168 168 """Return a generator that renders old-style list template
169 169
170 170 name is name of key in template map.
171 171 values is list of strings or dicts.
172 172 plural is plural of name, if not simply name + 's'.
173 173 separator is used to join values as a string
174 174
175 175 expansion works like this, given name 'foo'.
176 176
177 177 if values is empty, expand 'no_foos'.
178 178
179 179 if 'foo' not in template map, return values as a string,
180 180 joined by 'separator'.
181 181
182 182 expand 'start_foos'.
183 183
184 184 for each value, expand 'foo'. if 'last_foo' in template
185 185 map, expand it instead of 'foo' for last key.
186 186
187 187 expand 'end_foos'.
188 188 """
189 189 if not plural:
190 190 plural = name + 's'
191 191 if not values:
192 192 noname = 'no_' + plural
193 193 if context.preload(noname):
194 194 yield context.process(noname, mapping)
195 195 return
196 196 if not context.preload(name):
197 197 if isinstance(values[0], bytes):
198 198 yield separator.join(values)
199 199 else:
200 200 for v in values:
201 201 r = dict(v)
202 202 r.update(mapping)
203 203 yield r
204 204 return
205 205 startname = 'start_' + plural
206 206 if context.preload(startname):
207 207 yield context.process(startname, mapping)
208 208 def one(v, tag=name):
209 209 vmapping = {}
210 210 try:
211 211 vmapping.update(v)
212 212 # Python 2 raises ValueError if the type of v is wrong. Python
213 213 # 3 raises TypeError.
214 214 except (AttributeError, TypeError, ValueError):
215 215 try:
216 216 # Python 2 raises ValueError trying to destructure an e.g.
217 217 # bytes. Python 3 raises TypeError.
218 218 for a, b in v:
219 219 vmapping[a] = b
220 220 except (TypeError, ValueError):
221 221 vmapping[name] = v
222 222 vmapping = context.overlaymap(mapping, vmapping)
223 223 return context.process(tag, vmapping)
224 224 lastname = 'last_' + name
225 225 if context.preload(lastname):
226 226 last = values.pop()
227 227 else:
228 228 last = None
229 229 for v in values:
230 230 yield one(v)
231 231 if last is not None:
232 232 yield one(last, tag=lastname)
233 233 endname = 'end_' + plural
234 234 if context.preload(endname):
235 235 yield context.process(endname, mapping)
236 236
237 237 def flatten(thing):
238 238 """Yield a single stream from a possibly nested set of iterators"""
239 239 thing = unwraphybrid(thing)
240 240 if isinstance(thing, bytes):
241 241 yield thing
242 242 elif isinstance(thing, str):
243 243 # We can only hit this on Python 3, and it's here to guard
244 244 # against infinite recursion.
245 245 raise error.ProgrammingError('Mercurial IO including templates is done'
246 246 ' with bytes, not strings, got %r' % thing)
247 247 elif thing is None:
248 248 pass
249 249 elif not util.safehasattr(thing, '__iter__'):
250 250 yield pycompat.bytestr(thing)
251 251 else:
252 252 for i in thing:
253 253 i = unwraphybrid(i)
254 254 if isinstance(i, bytes):
255 255 yield i
256 256 elif i is None:
257 257 pass
258 258 elif not util.safehasattr(i, '__iter__'):
259 259 yield pycompat.bytestr(i)
260 260 else:
261 261 for j in flatten(i):
262 262 yield j
263 263
264 264 def stringify(thing):
265 265 """Turn values into bytes by converting into text and concatenating them"""
266 thing = unwraphybrid(thing)
267 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
268 if isinstance(thing, str):
269 # This is only reachable on Python 3 (otherwise
270 # isinstance(thing, bytes) would have been true), and is
271 # here to prevent infinite recursion bugs on Python 3.
272 raise error.ProgrammingError(
273 'stringify got unexpected unicode string: %r' % thing)
274 return "".join([stringify(t) for t in thing if t is not None])
275 if thing is None:
276 return ""
277 return pycompat.bytestr(thing)
266 if isinstance(thing, bytes):
267 return thing # retain localstr to be round-tripped
268 return b''.join(flatten(thing))
278 269
279 270 def findsymbolicname(arg):
280 271 """Find symbolic name for the given compiled expression; returns None
281 272 if nothing found reliably"""
282 273 while True:
283 274 func, data = arg
284 275 if func is runsymbol:
285 276 return data
286 277 elif func is runfilter:
287 278 arg = data[0]
288 279 else:
289 280 return None
290 281
291 282 def evalrawexp(context, mapping, arg):
292 283 """Evaluate given argument as a bare template object which may require
293 284 further processing (such as folding generator of strings)"""
294 285 func, data = arg
295 286 return func(context, mapping, data)
296 287
297 288 def evalfuncarg(context, mapping, arg):
298 289 """Evaluate given argument as value type"""
299 290 thing = evalrawexp(context, mapping, arg)
300 291 thing = unwrapvalue(thing)
301 292 # evalrawexp() may return string, generator of strings or arbitrary object
302 293 # such as date tuple, but filter does not want generator.
303 294 if isinstance(thing, types.GeneratorType):
304 295 thing = stringify(thing)
305 296 return thing
306 297
307 298 def evalboolean(context, mapping, arg):
308 299 """Evaluate given argument as boolean, but also takes boolean literals"""
309 300 func, data = arg
310 301 if func is runsymbol:
311 302 thing = func(context, mapping, data, default=None)
312 303 if thing is None:
313 304 # not a template keyword, takes as a boolean literal
314 305 thing = stringutil.parsebool(data)
315 306 else:
316 307 thing = func(context, mapping, data)
317 308 thing = unwrapvalue(thing)
318 309 if isinstance(thing, bool):
319 310 return thing
320 311 # other objects are evaluated as strings, which means 0 is True, but
321 312 # empty dict/list should be False as they are expected to be ''
322 313 return bool(stringify(thing))
323 314
324 315 def evalinteger(context, mapping, arg, err=None):
325 316 v = evalfuncarg(context, mapping, arg)
326 317 try:
327 318 return int(v)
328 319 except (TypeError, ValueError):
329 320 raise error.ParseError(err or _('not an integer'))
330 321
331 322 def evalstring(context, mapping, arg):
332 323 return stringify(evalrawexp(context, mapping, arg))
333 324
334 325 def evalstringliteral(context, mapping, arg):
335 326 """Evaluate given argument as string template, but returns symbol name
336 327 if it is unknown"""
337 328 func, data = arg
338 329 if func is runsymbol:
339 330 thing = func(context, mapping, data, default=data)
340 331 else:
341 332 thing = func(context, mapping, data)
342 333 return stringify(thing)
343 334
344 335 _evalfuncbytype = {
345 336 bool: evalboolean,
346 337 bytes: evalstring,
347 338 int: evalinteger,
348 339 }
349 340
350 341 def evalastype(context, mapping, arg, typ):
351 342 """Evaluate given argument and coerce its type"""
352 343 try:
353 344 f = _evalfuncbytype[typ]
354 345 except KeyError:
355 346 raise error.ProgrammingError('invalid type specified: %r' % typ)
356 347 return f(context, mapping, arg)
357 348
358 349 def runinteger(context, mapping, data):
359 350 return int(data)
360 351
361 352 def runstring(context, mapping, data):
362 353 return data
363 354
364 355 def _recursivesymbolblocker(key):
365 356 def showrecursion(**args):
366 357 raise error.Abort(_("recursive reference '%s' in template") % key)
367 358 return showrecursion
368 359
369 360 def runsymbol(context, mapping, key, default=''):
370 361 v = context.symbol(mapping, key)
371 362 if v is None:
372 363 # put poison to cut recursion. we can't move this to parsing phase
373 364 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
374 365 safemapping = mapping.copy()
375 366 safemapping[key] = _recursivesymbolblocker(key)
376 367 try:
377 368 v = context.process(key, safemapping)
378 369 except TemplateNotFound:
379 370 v = default
380 371 if callable(v) and getattr(v, '_requires', None) is None:
381 372 # old templatekw: expand all keywords and resources
382 373 # (TODO: deprecate this after porting web template keywords to new API)
383 374 props = {k: context._resources.lookup(context, mapping, k)
384 375 for k in context._resources.knownkeys()}
385 376 # pass context to _showcompatlist() through templatekw._showlist()
386 377 props['templ'] = context
387 378 props.update(mapping)
388 379 return v(**pycompat.strkwargs(props))
389 380 if callable(v):
390 381 # new templatekw
391 382 try:
392 383 return v(context, mapping)
393 384 except ResourceUnavailable:
394 385 # unsupported keyword is mapped to empty just like unknown keyword
395 386 return None
396 387 return v
397 388
398 389 def runtemplate(context, mapping, template):
399 390 for arg in template:
400 391 yield evalrawexp(context, mapping, arg)
401 392
402 393 def runfilter(context, mapping, data):
403 394 arg, filt = data
404 395 thing = evalfuncarg(context, mapping, arg)
405 396 try:
406 397 return filt(thing)
407 398 except (ValueError, AttributeError, TypeError):
408 399 sym = findsymbolicname(arg)
409 400 if sym:
410 401 msg = (_("template filter '%s' is not compatible with keyword '%s'")
411 402 % (pycompat.sysbytes(filt.__name__), sym))
412 403 else:
413 404 msg = (_("incompatible use of template filter '%s'")
414 405 % pycompat.sysbytes(filt.__name__))
415 406 raise error.Abort(msg)
416 407
417 408 def runmap(context, mapping, data):
418 409 darg, targ = data
419 410 d = evalrawexp(context, mapping, darg)
420 411 if util.safehasattr(d, 'itermaps'):
421 412 diter = d.itermaps()
422 413 else:
423 414 try:
424 415 diter = iter(d)
425 416 except TypeError:
426 417 sym = findsymbolicname(darg)
427 418 if sym:
428 419 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
429 420 else:
430 421 raise error.ParseError(_("%r is not iterable") % d)
431 422
432 423 for i, v in enumerate(diter):
433 424 if isinstance(v, dict):
434 425 lm = context.overlaymap(mapping, v)
435 426 lm['index'] = i
436 427 yield evalrawexp(context, lm, targ)
437 428 else:
438 429 # v is not an iterable of dicts, this happen when 'key'
439 430 # has been fully expanded already and format is useless.
440 431 # If so, return the expanded value.
441 432 yield v
442 433
443 434 def runmember(context, mapping, data):
444 435 darg, memb = data
445 436 d = evalrawexp(context, mapping, darg)
446 437 if util.safehasattr(d, 'tomap'):
447 438 lm = context.overlaymap(mapping, d.tomap())
448 439 return runsymbol(context, lm, memb)
449 440 if util.safehasattr(d, 'get'):
450 441 return getdictitem(d, memb)
451 442
452 443 sym = findsymbolicname(darg)
453 444 if sym:
454 445 raise error.ParseError(_("keyword '%s' has no member") % sym)
455 446 else:
456 447 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
457 448
458 449 def runnegate(context, mapping, data):
459 450 data = evalinteger(context, mapping, data,
460 451 _('negation needs an integer argument'))
461 452 return -data
462 453
463 454 def runarithmetic(context, mapping, data):
464 455 func, left, right = data
465 456 left = evalinteger(context, mapping, left,
466 457 _('arithmetic only defined on integers'))
467 458 right = evalinteger(context, mapping, right,
468 459 _('arithmetic only defined on integers'))
469 460 try:
470 461 return func(left, right)
471 462 except ZeroDivisionError:
472 463 raise error.Abort(_('division by zero is not defined'))
473 464
474 465 def getdictitem(dictarg, key):
475 466 val = dictarg.get(key)
476 467 if val is None:
477 468 return
478 469 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now