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