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