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