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