##// END OF EJS Templates
templater: do dict lookup over a wrapped object...
Yuya Nishihara -
r38258:c2456a77 default
parent child Browse files
Show More
@@ -1,701 +1,701 b''
1 1 # templatefuncs.py - common template functions
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 re
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 wdirid,
16 16 )
17 17 from . import (
18 18 color,
19 19 encoding,
20 20 error,
21 21 minirst,
22 22 obsutil,
23 23 pycompat,
24 24 registrar,
25 25 revset as revsetmod,
26 26 revsetlang,
27 27 scmutil,
28 28 templatefilters,
29 29 templatekw,
30 30 templateutil,
31 31 util,
32 32 )
33 33 from .utils import (
34 34 dateutil,
35 35 stringutil,
36 36 )
37 37
38 38 evalrawexp = templateutil.evalrawexp
39 39 evalwrapped = templateutil.evalwrapped
40 40 evalfuncarg = templateutil.evalfuncarg
41 41 evalboolean = templateutil.evalboolean
42 42 evaldate = templateutil.evaldate
43 43 evalinteger = templateutil.evalinteger
44 44 evalstring = templateutil.evalstring
45 45 evalstringliteral = templateutil.evalstringliteral
46 46
47 47 # dict of template built-in functions
48 48 funcs = {}
49 49 templatefunc = registrar.templatefunc(funcs)
50 50
51 51 @templatefunc('date(date[, fmt])')
52 52 def date(context, mapping, args):
53 53 """Format a date. See :hg:`help dates` for formatting
54 54 strings. The default is a Unix date format, including the timezone:
55 55 "Mon Sep 04 15:13:13 2006 0700"."""
56 56 if not (1 <= len(args) <= 2):
57 57 # i18n: "date" is a keyword
58 58 raise error.ParseError(_("date expects one or two arguments"))
59 59
60 60 date = evaldate(context, mapping, args[0],
61 61 # i18n: "date" is a keyword
62 62 _("date expects a date information"))
63 63 fmt = None
64 64 if len(args) == 2:
65 65 fmt = evalstring(context, mapping, args[1])
66 66 if fmt is None:
67 67 return dateutil.datestr(date)
68 68 else:
69 69 return dateutil.datestr(date, fmt)
70 70
71 71 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
72 72 def dict_(context, mapping, args):
73 73 """Construct a dict from key-value pairs. A key may be omitted if
74 74 a value expression can provide an unambiguous name."""
75 75 data = util.sortdict()
76 76
77 77 for v in args['args']:
78 78 k = templateutil.findsymbolicname(v)
79 79 if not k:
80 80 raise error.ParseError(_('dict key cannot be inferred'))
81 81 if k in data or k in args['kwargs']:
82 82 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
83 83 data[k] = evalfuncarg(context, mapping, v)
84 84
85 85 data.update((k, evalfuncarg(context, mapping, v))
86 86 for k, v in args['kwargs'].iteritems())
87 87 return templateutil.hybriddict(data)
88 88
89 89 @templatefunc('diff([includepattern [, excludepattern]])')
90 90 def diff(context, mapping, args):
91 91 """Show a diff, optionally
92 92 specifying files to include or exclude."""
93 93 if len(args) > 2:
94 94 # i18n: "diff" is a keyword
95 95 raise error.ParseError(_("diff expects zero, one, or two arguments"))
96 96
97 97 def getpatterns(i):
98 98 if i < len(args):
99 99 s = evalstring(context, mapping, args[i]).strip()
100 100 if s:
101 101 return [s]
102 102 return []
103 103
104 104 ctx = context.resource(mapping, 'ctx')
105 105 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
106 106
107 107 return ''.join(chunks)
108 108
109 109 @templatefunc('extdata(source)', argspec='source')
110 110 def extdata(context, mapping, args):
111 111 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
112 112 if 'source' not in args:
113 113 # i18n: "extdata" is a keyword
114 114 raise error.ParseError(_('extdata expects one argument'))
115 115
116 116 source = evalstring(context, mapping, args['source'])
117 117 if not source:
118 118 sym = templateutil.findsymbolicname(args['source'])
119 119 if sym:
120 120 raise error.ParseError(_('empty data source specified'),
121 121 hint=_("did you mean extdata('%s')?") % sym)
122 122 else:
123 123 raise error.ParseError(_('empty data source specified'))
124 124 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
125 125 ctx = context.resource(mapping, 'ctx')
126 126 if source in cache:
127 127 data = cache[source]
128 128 else:
129 129 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
130 130 return data.get(ctx.rev(), '')
131 131
132 132 @templatefunc('files(pattern)')
133 133 def files(context, mapping, args):
134 134 """All files of the current changeset matching the pattern. See
135 135 :hg:`help patterns`."""
136 136 if not len(args) == 1:
137 137 # i18n: "files" is a keyword
138 138 raise error.ParseError(_("files expects one argument"))
139 139
140 140 raw = evalstring(context, mapping, args[0])
141 141 ctx = context.resource(mapping, 'ctx')
142 142 m = ctx.match([raw])
143 143 files = list(ctx.matches(m))
144 144 return templateutil.compatlist(context, mapping, "file", files)
145 145
146 146 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
147 147 def fill(context, mapping, args):
148 148 """Fill many
149 149 paragraphs with optional indentation. See the "fill" filter."""
150 150 if not (1 <= len(args) <= 4):
151 151 # i18n: "fill" is a keyword
152 152 raise error.ParseError(_("fill expects one to four arguments"))
153 153
154 154 text = evalstring(context, mapping, args[0])
155 155 width = 76
156 156 initindent = ''
157 157 hangindent = ''
158 158 if 2 <= len(args) <= 4:
159 159 width = evalinteger(context, mapping, args[1],
160 160 # i18n: "fill" is a keyword
161 161 _("fill expects an integer width"))
162 162 try:
163 163 initindent = evalstring(context, mapping, args[2])
164 164 hangindent = evalstring(context, mapping, args[3])
165 165 except IndexError:
166 166 pass
167 167
168 168 return templatefilters.fill(text, width, initindent, hangindent)
169 169
170 170 @templatefunc('formatnode(node)')
171 171 def formatnode(context, mapping, args):
172 172 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
173 173 if len(args) != 1:
174 174 # i18n: "formatnode" is a keyword
175 175 raise error.ParseError(_("formatnode expects one argument"))
176 176
177 177 ui = context.resource(mapping, 'ui')
178 178 node = evalstring(context, mapping, args[0])
179 179 if ui.debugflag:
180 180 return node
181 181 return templatefilters.short(node)
182 182
183 183 @templatefunc('mailmap(author)')
184 184 def mailmap(context, mapping, args):
185 185 """Return the author, updated according to the value
186 186 set in the .mailmap file"""
187 187 if len(args) != 1:
188 188 raise error.ParseError(_("mailmap expects one argument"))
189 189
190 190 author = evalstring(context, mapping, args[0])
191 191
192 192 cache = context.resource(mapping, 'cache')
193 193 repo = context.resource(mapping, 'repo')
194 194
195 195 if 'mailmap' not in cache:
196 196 data = repo.wvfs.tryread('.mailmap')
197 197 cache['mailmap'] = stringutil.parsemailmap(data)
198 198
199 199 return stringutil.mapname(cache['mailmap'], author)
200 200
201 201 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
202 202 argspec='text width fillchar left')
203 203 def pad(context, mapping, args):
204 204 """Pad text with a
205 205 fill character."""
206 206 if 'text' not in args or 'width' not in args:
207 207 # i18n: "pad" is a keyword
208 208 raise error.ParseError(_("pad() expects two to four arguments"))
209 209
210 210 width = evalinteger(context, mapping, args['width'],
211 211 # i18n: "pad" is a keyword
212 212 _("pad() expects an integer width"))
213 213
214 214 text = evalstring(context, mapping, args['text'])
215 215
216 216 left = False
217 217 fillchar = ' '
218 218 if 'fillchar' in args:
219 219 fillchar = evalstring(context, mapping, args['fillchar'])
220 220 if len(color.stripeffects(fillchar)) != 1:
221 221 # i18n: "pad" is a keyword
222 222 raise error.ParseError(_("pad() expects a single fill character"))
223 223 if 'left' in args:
224 224 left = evalboolean(context, mapping, args['left'])
225 225
226 226 fillwidth = width - encoding.colwidth(color.stripeffects(text))
227 227 if fillwidth <= 0:
228 228 return text
229 229 if left:
230 230 return fillchar * fillwidth + text
231 231 else:
232 232 return text + fillchar * fillwidth
233 233
234 234 @templatefunc('indent(text, indentchars[, firstline])')
235 235 def indent(context, mapping, args):
236 236 """Indents all non-empty lines
237 237 with the characters given in the indentchars string. An optional
238 238 third parameter will override the indent for the first line only
239 239 if present."""
240 240 if not (2 <= len(args) <= 3):
241 241 # i18n: "indent" is a keyword
242 242 raise error.ParseError(_("indent() expects two or three arguments"))
243 243
244 244 text = evalstring(context, mapping, args[0])
245 245 indent = evalstring(context, mapping, args[1])
246 246
247 247 if len(args) == 3:
248 248 firstline = evalstring(context, mapping, args[2])
249 249 else:
250 250 firstline = indent
251 251
252 252 # the indent function doesn't indent the first line, so we do it here
253 253 return templatefilters.indent(firstline + text, indent)
254 254
255 255 @templatefunc('get(dict, key)')
256 256 def get(context, mapping, args):
257 257 """Get an attribute/key from an object. Some keywords
258 258 are complex types. This function allows you to obtain the value of an
259 259 attribute on these types."""
260 260 if len(args) != 2:
261 261 # i18n: "get" is a keyword
262 262 raise error.ParseError(_("get() expects two arguments"))
263 263
264 dictarg = evalfuncarg(context, mapping, args[0])
264 dictarg = evalwrapped(context, mapping, args[0])
265 265 if not util.safehasattr(dictarg, 'get'):
266 266 # i18n: "get" is a keyword
267 267 raise error.ParseError(_("get() expects a dict as first argument"))
268 268
269 269 key = evalfuncarg(context, mapping, args[1])
270 270 return templateutil.getdictitem(dictarg, key)
271 271
272 272 @templatefunc('if(expr, then[, else])')
273 273 def if_(context, mapping, args):
274 274 """Conditionally execute based on the result of
275 275 an expression."""
276 276 if not (2 <= len(args) <= 3):
277 277 # i18n: "if" is a keyword
278 278 raise error.ParseError(_("if expects two or three arguments"))
279 279
280 280 test = evalboolean(context, mapping, args[0])
281 281 if test:
282 282 return evalrawexp(context, mapping, args[1])
283 283 elif len(args) == 3:
284 284 return evalrawexp(context, mapping, args[2])
285 285
286 286 @templatefunc('ifcontains(needle, haystack, then[, else])')
287 287 def ifcontains(context, mapping, args):
288 288 """Conditionally execute based
289 289 on whether the item "needle" is in "haystack"."""
290 290 if not (3 <= len(args) <= 4):
291 291 # i18n: "ifcontains" is a keyword
292 292 raise error.ParseError(_("ifcontains expects three or four arguments"))
293 293
294 294 haystack = evalfuncarg(context, mapping, args[1])
295 295 keytype = getattr(haystack, 'keytype', None)
296 296 try:
297 297 needle = evalrawexp(context, mapping, args[0])
298 298 needle = templateutil.unwrapastype(context, mapping, needle,
299 299 keytype or bytes)
300 300 found = (needle in haystack)
301 301 except error.ParseError:
302 302 found = False
303 303
304 304 if found:
305 305 return evalrawexp(context, mapping, args[2])
306 306 elif len(args) == 4:
307 307 return evalrawexp(context, mapping, args[3])
308 308
309 309 @templatefunc('ifeq(expr1, expr2, then[, else])')
310 310 def ifeq(context, mapping, args):
311 311 """Conditionally execute based on
312 312 whether 2 items are equivalent."""
313 313 if not (3 <= len(args) <= 4):
314 314 # i18n: "ifeq" is a keyword
315 315 raise error.ParseError(_("ifeq expects three or four arguments"))
316 316
317 317 test = evalstring(context, mapping, args[0])
318 318 match = evalstring(context, mapping, args[1])
319 319 if test == match:
320 320 return evalrawexp(context, mapping, args[2])
321 321 elif len(args) == 4:
322 322 return evalrawexp(context, mapping, args[3])
323 323
324 324 @templatefunc('join(list, sep)')
325 325 def join(context, mapping, args):
326 326 """Join items in a list with a delimiter."""
327 327 if not (1 <= len(args) <= 2):
328 328 # i18n: "join" is a keyword
329 329 raise error.ParseError(_("join expects one or two arguments"))
330 330
331 331 joinset = evalwrapped(context, mapping, args[0])
332 332 joiner = " "
333 333 if len(args) > 1:
334 334 joiner = evalstring(context, mapping, args[1])
335 335 return joinset.join(context, mapping, joiner)
336 336
337 337 @templatefunc('label(label, expr)')
338 338 def label(context, mapping, args):
339 339 """Apply a label to generated content. Content with
340 340 a label applied can result in additional post-processing, such as
341 341 automatic colorization."""
342 342 if len(args) != 2:
343 343 # i18n: "label" is a keyword
344 344 raise error.ParseError(_("label expects two arguments"))
345 345
346 346 ui = context.resource(mapping, 'ui')
347 347 thing = evalstring(context, mapping, args[1])
348 348 # preserve unknown symbol as literal so effects like 'red', 'bold',
349 349 # etc. don't need to be quoted
350 350 label = evalstringliteral(context, mapping, args[0])
351 351
352 352 return ui.label(thing, label)
353 353
354 354 @templatefunc('latesttag([pattern])')
355 355 def latesttag(context, mapping, args):
356 356 """The global tags matching the given pattern on the
357 357 most recent globally tagged ancestor of this changeset.
358 358 If no such tags exist, the "{tag}" template resolves to
359 359 the string "null". See :hg:`help revisions.patterns` for the pattern
360 360 syntax.
361 361 """
362 362 if len(args) > 1:
363 363 # i18n: "latesttag" is a keyword
364 364 raise error.ParseError(_("latesttag expects at most one argument"))
365 365
366 366 pattern = None
367 367 if len(args) == 1:
368 368 pattern = evalstring(context, mapping, args[0])
369 369 return templatekw.showlatesttags(context, mapping, pattern)
370 370
371 371 @templatefunc('localdate(date[, tz])')
372 372 def localdate(context, mapping, args):
373 373 """Converts a date to the specified timezone.
374 374 The default is local date."""
375 375 if not (1 <= len(args) <= 2):
376 376 # i18n: "localdate" is a keyword
377 377 raise error.ParseError(_("localdate expects one or two arguments"))
378 378
379 379 date = evaldate(context, mapping, args[0],
380 380 # i18n: "localdate" is a keyword
381 381 _("localdate expects a date information"))
382 382 if len(args) >= 2:
383 383 tzoffset = None
384 384 tz = evalfuncarg(context, mapping, args[1])
385 385 if isinstance(tz, bytes):
386 386 tzoffset, remainder = dateutil.parsetimezone(tz)
387 387 if remainder:
388 388 tzoffset = None
389 389 if tzoffset is None:
390 390 try:
391 391 tzoffset = int(tz)
392 392 except (TypeError, ValueError):
393 393 # i18n: "localdate" is a keyword
394 394 raise error.ParseError(_("localdate expects a timezone"))
395 395 else:
396 396 tzoffset = dateutil.makedate()[1]
397 397 return (date[0], tzoffset)
398 398
399 399 @templatefunc('max(iterable)')
400 400 def max_(context, mapping, args, **kwargs):
401 401 """Return the max of an iterable"""
402 402 if len(args) != 1:
403 403 # i18n: "max" is a keyword
404 404 raise error.ParseError(_("max expects one argument"))
405 405
406 406 iterable = evalfuncarg(context, mapping, args[0])
407 407 try:
408 408 x = max(pycompat.maybebytestr(iterable))
409 409 except (TypeError, ValueError):
410 410 # i18n: "max" is a keyword
411 411 raise error.ParseError(_("max first argument should be an iterable"))
412 412 return templateutil.wraphybridvalue(iterable, x, x)
413 413
414 414 @templatefunc('min(iterable)')
415 415 def min_(context, mapping, args, **kwargs):
416 416 """Return the min of an iterable"""
417 417 if len(args) != 1:
418 418 # i18n: "min" is a keyword
419 419 raise error.ParseError(_("min expects one argument"))
420 420
421 421 iterable = evalfuncarg(context, mapping, args[0])
422 422 try:
423 423 x = min(pycompat.maybebytestr(iterable))
424 424 except (TypeError, ValueError):
425 425 # i18n: "min" is a keyword
426 426 raise error.ParseError(_("min first argument should be an iterable"))
427 427 return templateutil.wraphybridvalue(iterable, x, x)
428 428
429 429 @templatefunc('mod(a, b)')
430 430 def mod(context, mapping, args):
431 431 """Calculate a mod b such that a / b + a mod b == a"""
432 432 if not len(args) == 2:
433 433 # i18n: "mod" is a keyword
434 434 raise error.ParseError(_("mod expects two arguments"))
435 435
436 436 func = lambda a, b: a % b
437 437 return templateutil.runarithmetic(context, mapping,
438 438 (func, args[0], args[1]))
439 439
440 440 @templatefunc('obsfateoperations(markers)')
441 441 def obsfateoperations(context, mapping, args):
442 442 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
443 443 if len(args) != 1:
444 444 # i18n: "obsfateoperations" is a keyword
445 445 raise error.ParseError(_("obsfateoperations expects one argument"))
446 446
447 447 markers = evalfuncarg(context, mapping, args[0])
448 448
449 449 try:
450 450 data = obsutil.markersoperations(markers)
451 451 return templateutil.hybridlist(data, name='operation')
452 452 except (TypeError, KeyError):
453 453 # i18n: "obsfateoperations" is a keyword
454 454 errmsg = _("obsfateoperations first argument should be an iterable")
455 455 raise error.ParseError(errmsg)
456 456
457 457 @templatefunc('obsfatedate(markers)')
458 458 def obsfatedate(context, mapping, args):
459 459 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
460 460 if len(args) != 1:
461 461 # i18n: "obsfatedate" is a keyword
462 462 raise error.ParseError(_("obsfatedate expects one argument"))
463 463
464 464 markers = evalfuncarg(context, mapping, args[0])
465 465
466 466 try:
467 467 data = obsutil.markersdates(markers)
468 468 return templateutil.hybridlist(data, name='date', fmt='%d %d')
469 469 except (TypeError, KeyError):
470 470 # i18n: "obsfatedate" is a keyword
471 471 errmsg = _("obsfatedate first argument should be an iterable")
472 472 raise error.ParseError(errmsg)
473 473
474 474 @templatefunc('obsfateusers(markers)')
475 475 def obsfateusers(context, mapping, args):
476 476 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
477 477 if len(args) != 1:
478 478 # i18n: "obsfateusers" is a keyword
479 479 raise error.ParseError(_("obsfateusers expects one argument"))
480 480
481 481 markers = evalfuncarg(context, mapping, args[0])
482 482
483 483 try:
484 484 data = obsutil.markersusers(markers)
485 485 return templateutil.hybridlist(data, name='user')
486 486 except (TypeError, KeyError, ValueError):
487 487 # i18n: "obsfateusers" is a keyword
488 488 msg = _("obsfateusers first argument should be an iterable of "
489 489 "obsmakers")
490 490 raise error.ParseError(msg)
491 491
492 492 @templatefunc('obsfateverb(successors, markers)')
493 493 def obsfateverb(context, mapping, args):
494 494 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
495 495 if len(args) != 2:
496 496 # i18n: "obsfateverb" is a keyword
497 497 raise error.ParseError(_("obsfateverb expects two arguments"))
498 498
499 499 successors = evalfuncarg(context, mapping, args[0])
500 500 markers = evalfuncarg(context, mapping, args[1])
501 501
502 502 try:
503 503 return obsutil.obsfateverb(successors, markers)
504 504 except TypeError:
505 505 # i18n: "obsfateverb" is a keyword
506 506 errmsg = _("obsfateverb first argument should be countable")
507 507 raise error.ParseError(errmsg)
508 508
509 509 @templatefunc('relpath(path)')
510 510 def relpath(context, mapping, args):
511 511 """Convert a repository-absolute path into a filesystem path relative to
512 512 the current working directory."""
513 513 if len(args) != 1:
514 514 # i18n: "relpath" is a keyword
515 515 raise error.ParseError(_("relpath expects one argument"))
516 516
517 517 repo = context.resource(mapping, 'ctx').repo()
518 518 path = evalstring(context, mapping, args[0])
519 519 return repo.pathto(path)
520 520
521 521 @templatefunc('revset(query[, formatargs...])')
522 522 def revset(context, mapping, args):
523 523 """Execute a revision set query. See
524 524 :hg:`help revset`."""
525 525 if not len(args) > 0:
526 526 # i18n: "revset" is a keyword
527 527 raise error.ParseError(_("revset expects one or more arguments"))
528 528
529 529 raw = evalstring(context, mapping, args[0])
530 530 ctx = context.resource(mapping, 'ctx')
531 531 repo = ctx.repo()
532 532
533 533 def query(expr):
534 534 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
535 535 return m(repo)
536 536
537 537 if len(args) > 1:
538 538 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
539 539 revs = query(revsetlang.formatspec(raw, *formatargs))
540 540 revs = list(revs)
541 541 else:
542 542 cache = context.resource(mapping, 'cache')
543 543 revsetcache = cache.setdefault("revsetcache", {})
544 544 if raw in revsetcache:
545 545 revs = revsetcache[raw]
546 546 else:
547 547 revs = query(raw)
548 548 revs = list(revs)
549 549 revsetcache[raw] = revs
550 550 return templatekw.showrevslist(context, mapping, "revision", revs)
551 551
552 552 @templatefunc('rstdoc(text, style)')
553 553 def rstdoc(context, mapping, args):
554 554 """Format reStructuredText."""
555 555 if len(args) != 2:
556 556 # i18n: "rstdoc" is a keyword
557 557 raise error.ParseError(_("rstdoc expects two arguments"))
558 558
559 559 text = evalstring(context, mapping, args[0])
560 560 style = evalstring(context, mapping, args[1])
561 561
562 562 return minirst.format(text, style=style, keep=['verbose'])[0]
563 563
564 564 @templatefunc('separate(sep, args...)', argspec='sep *args')
565 565 def separate(context, mapping, args):
566 566 """Add a separator between non-empty arguments."""
567 567 if 'sep' not in args:
568 568 # i18n: "separate" is a keyword
569 569 raise error.ParseError(_("separate expects at least one argument"))
570 570
571 571 sep = evalstring(context, mapping, args['sep'])
572 572 first = True
573 573 for arg in args['args']:
574 574 argstr = evalstring(context, mapping, arg)
575 575 if not argstr:
576 576 continue
577 577 if first:
578 578 first = False
579 579 else:
580 580 yield sep
581 581 yield argstr
582 582
583 583 @templatefunc('shortest(node, minlength=4)')
584 584 def shortest(context, mapping, args):
585 585 """Obtain the shortest representation of
586 586 a node."""
587 587 if not (1 <= len(args) <= 2):
588 588 # i18n: "shortest" is a keyword
589 589 raise error.ParseError(_("shortest() expects one or two arguments"))
590 590
591 591 hexnode = evalstring(context, mapping, args[0])
592 592
593 593 minlength = 4
594 594 if len(args) > 1:
595 595 minlength = evalinteger(context, mapping, args[1],
596 596 # i18n: "shortest" is a keyword
597 597 _("shortest() expects an integer minlength"))
598 598
599 599 repo = context.resource(mapping, 'ctx')._repo
600 600 if len(hexnode) > 40:
601 601 return hexnode
602 602 elif len(hexnode) == 40:
603 603 try:
604 604 node = bin(hexnode)
605 605 except TypeError:
606 606 return hexnode
607 607 else:
608 608 try:
609 609 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
610 610 except error.WdirUnsupported:
611 611 node = wdirid
612 612 except error.LookupError:
613 613 return hexnode
614 614 if not node:
615 615 return hexnode
616 616 try:
617 617 return scmutil.shortesthexnodeidprefix(repo, node, minlength)
618 618 except error.RepoLookupError:
619 619 return hexnode
620 620
621 621 @templatefunc('strip(text[, chars])')
622 622 def strip(context, mapping, args):
623 623 """Strip characters from a string. By default,
624 624 strips all leading and trailing whitespace."""
625 625 if not (1 <= len(args) <= 2):
626 626 # i18n: "strip" is a keyword
627 627 raise error.ParseError(_("strip expects one or two arguments"))
628 628
629 629 text = evalstring(context, mapping, args[0])
630 630 if len(args) == 2:
631 631 chars = evalstring(context, mapping, args[1])
632 632 return text.strip(chars)
633 633 return text.strip()
634 634
635 635 @templatefunc('sub(pattern, replacement, expression)')
636 636 def sub(context, mapping, args):
637 637 """Perform text substitution
638 638 using regular expressions."""
639 639 if len(args) != 3:
640 640 # i18n: "sub" is a keyword
641 641 raise error.ParseError(_("sub expects three arguments"))
642 642
643 643 pat = evalstring(context, mapping, args[0])
644 644 rpl = evalstring(context, mapping, args[1])
645 645 src = evalstring(context, mapping, args[2])
646 646 try:
647 647 patre = re.compile(pat)
648 648 except re.error:
649 649 # i18n: "sub" is a keyword
650 650 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
651 651 try:
652 652 yield patre.sub(rpl, src)
653 653 except re.error:
654 654 # i18n: "sub" is a keyword
655 655 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
656 656
657 657 @templatefunc('startswith(pattern, text)')
658 658 def startswith(context, mapping, args):
659 659 """Returns the value from the "text" argument
660 660 if it begins with the content from the "pattern" argument."""
661 661 if len(args) != 2:
662 662 # i18n: "startswith" is a keyword
663 663 raise error.ParseError(_("startswith expects two arguments"))
664 664
665 665 patn = evalstring(context, mapping, args[0])
666 666 text = evalstring(context, mapping, args[1])
667 667 if text.startswith(patn):
668 668 return text
669 669 return ''
670 670
671 671 @templatefunc('word(number, text[, separator])')
672 672 def word(context, mapping, args):
673 673 """Return the nth word from a string."""
674 674 if not (2 <= len(args) <= 3):
675 675 # i18n: "word" is a keyword
676 676 raise error.ParseError(_("word expects two or three arguments, got %d")
677 677 % len(args))
678 678
679 679 num = evalinteger(context, mapping, args[0],
680 680 # i18n: "word" is a keyword
681 681 _("word expects an integer index"))
682 682 text = evalstring(context, mapping, args[1])
683 683 if len(args) == 3:
684 684 splitter = evalstring(context, mapping, args[2])
685 685 else:
686 686 splitter = None
687 687
688 688 tokens = text.split(splitter)
689 689 if num >= len(tokens) or num < -len(tokens):
690 690 return ''
691 691 else:
692 692 return tokens[num]
693 693
694 694 def loadfunction(ui, extname, registrarobj):
695 695 """Load template function from specified registrarobj
696 696 """
697 697 for name, func in registrarobj._table.iteritems():
698 698 funcs[name] = func
699 699
700 700 # tell hggettext to extract docstrings from these functions:
701 701 i18nfunctions = funcs.values()
@@ -1,708 +1,708 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 class wrappedbytes(wrapped):
70 70 """Wrapper for byte string"""
71 71
72 72 def __init__(self, value):
73 73 self._value = value
74 74
75 75 def itermaps(self, context):
76 76 raise error.ParseError(_('%r is not iterable of mappings')
77 77 % pycompat.bytestr(self._value))
78 78
79 79 def join(self, context, mapping, sep):
80 80 return joinitems(pycompat.iterbytestr(self._value), sep)
81 81
82 82 def show(self, context, mapping):
83 83 return self._value
84 84
85 85 def tovalue(self, context, mapping):
86 86 return self._value
87 87
88 88 class wrappedvalue(wrapped):
89 89 """Generic wrapper for pure non-list/dict/bytes value"""
90 90
91 91 def __init__(self, value):
92 92 self._value = value
93 93
94 94 def itermaps(self, context):
95 95 raise error.ParseError(_('%r is not iterable of mappings')
96 96 % self._value)
97 97
98 98 def join(self, context, mapping, sep):
99 99 raise error.ParseError(_('%r is not iterable') % self._value)
100 100
101 101 def show(self, context, mapping):
102 102 return pycompat.bytestr(self._value)
103 103
104 104 def tovalue(self, context, mapping):
105 105 return self._value
106 106
107 107 # stub for representing a date type; may be a real date type that can
108 108 # provide a readable string value
109 109 class date(object):
110 110 pass
111 111
112 112 class hybrid(wrapped):
113 113 """Wrapper for list or dict to support legacy template
114 114
115 115 This class allows us to handle both:
116 116 - "{files}" (legacy command-line-specific list hack) and
117 117 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
118 118 and to access raw values:
119 119 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
120 120 - "{get(extras, key)}"
121 121 - "{files|json}"
122 122 """
123 123
124 124 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
125 125 self._gen = gen # generator or function returning generator
126 126 self._values = values
127 127 self._makemap = makemap
128 128 self._joinfmt = joinfmt
129 129 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
130 130
131 131 def itermaps(self, context):
132 132 makemap = self._makemap
133 133 for x in self._values:
134 134 yield makemap(x)
135 135
136 136 def join(self, context, mapping, sep):
137 137 # TODO: switch gen to (context, mapping) API?
138 138 return joinitems((self._joinfmt(x) for x in self._values), sep)
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 self.join(context, mapping, ' ')
145 145 if callable(gen):
146 146 return gen()
147 147 return gen
148 148
149 149 def tovalue(self, context, mapping):
150 150 # TODO: return self._values and get rid of proxy methods
151 151 return self
152 152
153 153 def __contains__(self, x):
154 154 return x in self._values
155 155 def __getitem__(self, key):
156 156 return self._values[key]
157 157 def __len__(self):
158 158 return len(self._values)
159 159 def __iter__(self):
160 160 return iter(self._values)
161 161 def __getattr__(self, name):
162 162 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
163 163 r'itervalues', r'keys', r'values'):
164 164 raise AttributeError(name)
165 165 return getattr(self._values, name)
166 166
167 167 class mappable(wrapped):
168 168 """Wrapper for non-list/dict object to support map operation
169 169
170 170 This class allows us to handle both:
171 171 - "{manifest}"
172 172 - "{manifest % '{rev}:{node}'}"
173 173 - "{manifest.rev}"
174 174
175 175 Unlike a hybrid, this does not simulate the behavior of the underling
176 176 value.
177 177 """
178 178
179 179 def __init__(self, gen, key, value, makemap):
180 180 self._gen = gen # generator or function returning generator
181 181 self._key = key
182 182 self._value = value # may be generator of strings
183 183 self._makemap = makemap
184 184
185 185 def tomap(self):
186 186 return self._makemap(self._key)
187 187
188 188 def itermaps(self, context):
189 189 yield self.tomap()
190 190
191 191 def join(self, context, mapping, sep):
192 192 w = makewrapped(context, mapping, self._value)
193 193 return w.join(context, mapping, sep)
194 194
195 195 def show(self, context, mapping):
196 196 # TODO: switch gen to (context, mapping) API?
197 197 gen = self._gen
198 198 if gen is None:
199 199 return pycompat.bytestr(self._value)
200 200 if callable(gen):
201 201 return gen()
202 202 return gen
203 203
204 204 def tovalue(self, context, mapping):
205 205 return _unthunk(context, mapping, self._value)
206 206
207 207 class _mappingsequence(wrapped):
208 208 """Wrapper for sequence of template mappings
209 209
210 210 This represents an inner template structure (i.e. a list of dicts),
211 211 which can also be rendered by the specified named/literal template.
212 212
213 213 Template mappings may be nested.
214 214 """
215 215
216 216 def __init__(self, name=None, tmpl=None, sep=''):
217 217 if name is not None and tmpl is not None:
218 218 raise error.ProgrammingError('name and tmpl are mutually exclusive')
219 219 self._name = name
220 220 self._tmpl = tmpl
221 221 self._defaultsep = sep
222 222
223 223 def join(self, context, mapping, sep):
224 224 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
225 225 if self._name:
226 226 itemiter = (context.process(self._name, m) for m in mapsiter)
227 227 elif self._tmpl:
228 228 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
229 229 else:
230 230 raise error.ParseError(_('not displayable without template'))
231 231 return joinitems(itemiter, sep)
232 232
233 233 def show(self, context, mapping):
234 234 return self.join(context, mapping, self._defaultsep)
235 235
236 236 def tovalue(self, context, mapping):
237 237 knownres = context.knownresourcekeys()
238 238 items = []
239 239 for nm in self.itermaps(context):
240 240 # drop internal resources (recursively) which shouldn't be displayed
241 241 lm = context.overlaymap(mapping, nm)
242 242 items.append({k: unwrapvalue(context, lm, v)
243 243 for k, v in nm.iteritems() if k not in knownres})
244 244 return items
245 245
246 246 class mappinggenerator(_mappingsequence):
247 247 """Wrapper for generator of template mappings
248 248
249 249 The function ``make(context, *args)`` should return a generator of
250 250 mapping dicts.
251 251 """
252 252
253 253 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
254 254 super(mappinggenerator, self).__init__(name, tmpl, sep)
255 255 self._make = make
256 256 self._args = args
257 257
258 258 def itermaps(self, context):
259 259 return self._make(context, *self._args)
260 260
261 261 class mappinglist(_mappingsequence):
262 262 """Wrapper for list of template mappings"""
263 263
264 264 def __init__(self, mappings, name=None, tmpl=None, sep=''):
265 265 super(mappinglist, self).__init__(name, tmpl, sep)
266 266 self._mappings = mappings
267 267
268 268 def itermaps(self, context):
269 269 return iter(self._mappings)
270 270
271 271 class mappedgenerator(wrapped):
272 272 """Wrapper for generator of strings which acts as a list
273 273
274 274 The function ``make(context, *args)`` should return a generator of
275 275 byte strings, or a generator of (possibly nested) generators of byte
276 276 strings (i.e. a generator for a list of byte strings.)
277 277 """
278 278
279 279 def __init__(self, make, args=()):
280 280 self._make = make
281 281 self._args = args
282 282
283 283 def _gen(self, context):
284 284 return self._make(context, *self._args)
285 285
286 286 def itermaps(self, context):
287 287 raise error.ParseError(_('list of strings is not mappable'))
288 288
289 289 def join(self, context, mapping, sep):
290 290 return joinitems(self._gen(context), sep)
291 291
292 292 def show(self, context, mapping):
293 293 return self.join(context, mapping, '')
294 294
295 295 def tovalue(self, context, mapping):
296 296 return [stringify(context, mapping, x) for x in self._gen(context)]
297 297
298 298 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
299 299 """Wrap data to support both dict-like and string-like operations"""
300 300 prefmt = pycompat.identity
301 301 if fmt is None:
302 302 fmt = '%s=%s'
303 303 prefmt = pycompat.bytestr
304 304 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
305 305 lambda k: fmt % (prefmt(k), prefmt(data[k])))
306 306
307 307 def hybridlist(data, name, fmt=None, gen=None):
308 308 """Wrap data to support both list-like and string-like operations"""
309 309 prefmt = pycompat.identity
310 310 if fmt is None:
311 311 fmt = '%s'
312 312 prefmt = pycompat.bytestr
313 313 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
314 314
315 315 def unwraphybrid(context, mapping, thing):
316 316 """Return an object which can be stringified possibly by using a legacy
317 317 template"""
318 318 if not isinstance(thing, wrapped):
319 319 return thing
320 320 return thing.show(context, mapping)
321 321
322 322 def wraphybridvalue(container, key, value):
323 323 """Wrap an element of hybrid container to be mappable
324 324
325 325 The key is passed to the makemap function of the given container, which
326 326 should be an item generated by iter(container).
327 327 """
328 328 makemap = getattr(container, '_makemap', None)
329 329 if makemap is None:
330 330 return value
331 331 if util.safehasattr(value, '_makemap'):
332 332 # a nested hybrid list/dict, which has its own way of map operation
333 333 return value
334 334 return mappable(None, key, value, makemap)
335 335
336 336 def compatdict(context, mapping, name, data, key='key', value='value',
337 337 fmt=None, plural=None, separator=' '):
338 338 """Wrap data like hybriddict(), but also supports old-style list template
339 339
340 340 This exists for backward compatibility with the old-style template. Use
341 341 hybriddict() for new template keywords.
342 342 """
343 343 c = [{key: k, value: v} for k, v in data.iteritems()]
344 344 f = _showcompatlist(context, mapping, name, c, plural, separator)
345 345 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
346 346
347 347 def compatlist(context, mapping, name, data, element=None, fmt=None,
348 348 plural=None, separator=' '):
349 349 """Wrap data like hybridlist(), but also supports old-style list template
350 350
351 351 This exists for backward compatibility with the old-style template. Use
352 352 hybridlist() for new template keywords.
353 353 """
354 354 f = _showcompatlist(context, mapping, name, data, plural, separator)
355 355 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
356 356
357 357 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
358 358 """Return a generator that renders old-style list template
359 359
360 360 name is name of key in template map.
361 361 values is list of strings or dicts.
362 362 plural is plural of name, if not simply name + 's'.
363 363 separator is used to join values as a string
364 364
365 365 expansion works like this, given name 'foo'.
366 366
367 367 if values is empty, expand 'no_foos'.
368 368
369 369 if 'foo' not in template map, return values as a string,
370 370 joined by 'separator'.
371 371
372 372 expand 'start_foos'.
373 373
374 374 for each value, expand 'foo'. if 'last_foo' in template
375 375 map, expand it instead of 'foo' for last key.
376 376
377 377 expand 'end_foos'.
378 378 """
379 379 if not plural:
380 380 plural = name + 's'
381 381 if not values:
382 382 noname = 'no_' + plural
383 383 if context.preload(noname):
384 384 yield context.process(noname, mapping)
385 385 return
386 386 if not context.preload(name):
387 387 if isinstance(values[0], bytes):
388 388 yield separator.join(values)
389 389 else:
390 390 for v in values:
391 391 r = dict(v)
392 392 r.update(mapping)
393 393 yield r
394 394 return
395 395 startname = 'start_' + plural
396 396 if context.preload(startname):
397 397 yield context.process(startname, mapping)
398 398 def one(v, tag=name):
399 399 vmapping = {}
400 400 try:
401 401 vmapping.update(v)
402 402 # Python 2 raises ValueError if the type of v is wrong. Python
403 403 # 3 raises TypeError.
404 404 except (AttributeError, TypeError, ValueError):
405 405 try:
406 406 # Python 2 raises ValueError trying to destructure an e.g.
407 407 # bytes. Python 3 raises TypeError.
408 408 for a, b in v:
409 409 vmapping[a] = b
410 410 except (TypeError, ValueError):
411 411 vmapping[name] = v
412 412 vmapping = context.overlaymap(mapping, vmapping)
413 413 return context.process(tag, vmapping)
414 414 lastname = 'last_' + name
415 415 if context.preload(lastname):
416 416 last = values.pop()
417 417 else:
418 418 last = None
419 419 for v in values:
420 420 yield one(v)
421 421 if last is not None:
422 422 yield one(last, tag=lastname)
423 423 endname = 'end_' + plural
424 424 if context.preload(endname):
425 425 yield context.process(endname, mapping)
426 426
427 427 def flatten(context, mapping, thing):
428 428 """Yield a single stream from a possibly nested set of iterators"""
429 429 thing = unwraphybrid(context, mapping, thing)
430 430 if isinstance(thing, bytes):
431 431 yield thing
432 432 elif isinstance(thing, str):
433 433 # We can only hit this on Python 3, and it's here to guard
434 434 # against infinite recursion.
435 435 raise error.ProgrammingError('Mercurial IO including templates is done'
436 436 ' with bytes, not strings, got %r' % thing)
437 437 elif thing is None:
438 438 pass
439 439 elif not util.safehasattr(thing, '__iter__'):
440 440 yield pycompat.bytestr(thing)
441 441 else:
442 442 for i in thing:
443 443 i = unwraphybrid(context, mapping, i)
444 444 if isinstance(i, bytes):
445 445 yield i
446 446 elif i is None:
447 447 pass
448 448 elif not util.safehasattr(i, '__iter__'):
449 449 yield pycompat.bytestr(i)
450 450 else:
451 451 for j in flatten(context, mapping, i):
452 452 yield j
453 453
454 454 def stringify(context, mapping, thing):
455 455 """Turn values into bytes by converting into text and concatenating them"""
456 456 if isinstance(thing, bytes):
457 457 return thing # retain localstr to be round-tripped
458 458 return b''.join(flatten(context, mapping, thing))
459 459
460 460 def findsymbolicname(arg):
461 461 """Find symbolic name for the given compiled expression; returns None
462 462 if nothing found reliably"""
463 463 while True:
464 464 func, data = arg
465 465 if func is runsymbol:
466 466 return data
467 467 elif func is runfilter:
468 468 arg = data[0]
469 469 else:
470 470 return None
471 471
472 472 def _unthunk(context, mapping, thing):
473 473 """Evaluate a lazy byte string into value"""
474 474 if not isinstance(thing, types.GeneratorType):
475 475 return thing
476 476 return stringify(context, mapping, thing)
477 477
478 478 def evalrawexp(context, mapping, arg):
479 479 """Evaluate given argument as a bare template object which may require
480 480 further processing (such as folding generator of strings)"""
481 481 func, data = arg
482 482 return func(context, mapping, data)
483 483
484 484 def evalwrapped(context, mapping, arg):
485 485 """Evaluate given argument to wrapped object"""
486 486 thing = evalrawexp(context, mapping, arg)
487 487 return makewrapped(context, mapping, thing)
488 488
489 489 def makewrapped(context, mapping, thing):
490 490 """Lift object to a wrapped type"""
491 491 if isinstance(thing, wrapped):
492 492 return thing
493 493 thing = _unthunk(context, mapping, thing)
494 494 if isinstance(thing, bytes):
495 495 return wrappedbytes(thing)
496 496 return wrappedvalue(thing)
497 497
498 498 def evalfuncarg(context, mapping, arg):
499 499 """Evaluate given argument as value type"""
500 500 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
501 501
502 502 def unwrapvalue(context, mapping, thing):
503 503 """Move the inner value object out of the wrapper"""
504 504 if isinstance(thing, wrapped):
505 505 return thing.tovalue(context, mapping)
506 506 # evalrawexp() may return string, generator of strings or arbitrary object
507 507 # such as date tuple, but filter does not want generator.
508 508 return _unthunk(context, mapping, thing)
509 509
510 510 def evalboolean(context, mapping, arg):
511 511 """Evaluate given argument as boolean, but also takes boolean literals"""
512 512 func, data = arg
513 513 if func is runsymbol:
514 514 thing = func(context, mapping, data, default=None)
515 515 if thing is None:
516 516 # not a template keyword, takes as a boolean literal
517 517 thing = stringutil.parsebool(data)
518 518 else:
519 519 thing = func(context, mapping, data)
520 520 if isinstance(thing, wrapped):
521 521 thing = thing.tovalue(context, mapping)
522 522 if isinstance(thing, bool):
523 523 return thing
524 524 # other objects are evaluated as strings, which means 0 is True, but
525 525 # empty dict/list should be False as they are expected to be ''
526 526 return bool(stringify(context, mapping, thing))
527 527
528 528 def evaldate(context, mapping, arg, err=None):
529 529 """Evaluate given argument as a date tuple or a date string; returns
530 530 a (unixtime, offset) tuple"""
531 531 thing = evalrawexp(context, mapping, arg)
532 532 return unwrapdate(context, mapping, thing, err)
533 533
534 534 def unwrapdate(context, mapping, thing, err=None):
535 535 thing = unwrapvalue(context, mapping, thing)
536 536 try:
537 537 return dateutil.parsedate(thing)
538 538 except AttributeError:
539 539 raise error.ParseError(err or _('not a date tuple nor a string'))
540 540 except error.ParseError:
541 541 if not err:
542 542 raise
543 543 raise error.ParseError(err)
544 544
545 545 def evalinteger(context, mapping, arg, err=None):
546 546 thing = evalrawexp(context, mapping, arg)
547 547 return unwrapinteger(context, mapping, thing, err)
548 548
549 549 def unwrapinteger(context, mapping, thing, err=None):
550 550 thing = unwrapvalue(context, mapping, thing)
551 551 try:
552 552 return int(thing)
553 553 except (TypeError, ValueError):
554 554 raise error.ParseError(err or _('not an integer'))
555 555
556 556 def evalstring(context, mapping, arg):
557 557 return stringify(context, mapping, evalrawexp(context, mapping, arg))
558 558
559 559 def evalstringliteral(context, mapping, arg):
560 560 """Evaluate given argument as string template, but returns symbol name
561 561 if it is unknown"""
562 562 func, data = arg
563 563 if func is runsymbol:
564 564 thing = func(context, mapping, data, default=data)
565 565 else:
566 566 thing = func(context, mapping, data)
567 567 return stringify(context, mapping, thing)
568 568
569 569 _unwrapfuncbytype = {
570 570 None: unwrapvalue,
571 571 bytes: stringify,
572 572 date: unwrapdate,
573 573 int: unwrapinteger,
574 574 }
575 575
576 576 def unwrapastype(context, mapping, thing, typ):
577 577 """Move the inner value object out of the wrapper and coerce its type"""
578 578 try:
579 579 f = _unwrapfuncbytype[typ]
580 580 except KeyError:
581 581 raise error.ProgrammingError('invalid type specified: %r' % typ)
582 582 return f(context, mapping, thing)
583 583
584 584 def runinteger(context, mapping, data):
585 585 return int(data)
586 586
587 587 def runstring(context, mapping, data):
588 588 return data
589 589
590 590 def _recursivesymbolblocker(key):
591 591 def showrecursion(**args):
592 592 raise error.Abort(_("recursive reference '%s' in template") % key)
593 593 return showrecursion
594 594
595 595 def runsymbol(context, mapping, key, default=''):
596 596 v = context.symbol(mapping, key)
597 597 if v is None:
598 598 # put poison to cut recursion. we can't move this to parsing phase
599 599 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
600 600 safemapping = mapping.copy()
601 601 safemapping[key] = _recursivesymbolblocker(key)
602 602 try:
603 603 v = context.process(key, safemapping)
604 604 except TemplateNotFound:
605 605 v = default
606 606 if callable(v) and getattr(v, '_requires', None) is None:
607 607 # old templatekw: expand all keywords and resources
608 608 # (TODO: deprecate this after porting web template keywords to new API)
609 609 props = {k: context._resources.lookup(context, mapping, k)
610 610 for k in context._resources.knownkeys()}
611 611 # pass context to _showcompatlist() through templatekw._showlist()
612 612 props['templ'] = context
613 613 props.update(mapping)
614 614 return v(**pycompat.strkwargs(props))
615 615 if callable(v):
616 616 # new templatekw
617 617 try:
618 618 return v(context, mapping)
619 619 except ResourceUnavailable:
620 620 # unsupported keyword is mapped to empty just like unknown keyword
621 621 return None
622 622 return v
623 623
624 624 def runtemplate(context, mapping, template):
625 625 for arg in template:
626 626 yield evalrawexp(context, mapping, arg)
627 627
628 628 def runfilter(context, mapping, data):
629 629 arg, filt = data
630 630 thing = evalrawexp(context, mapping, arg)
631 631 intype = getattr(filt, '_intype', None)
632 632 try:
633 633 thing = unwrapastype(context, mapping, thing, intype)
634 634 return filt(thing)
635 635 except error.ParseError as e:
636 636 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
637 637
638 638 def _formatfiltererror(arg, filt):
639 639 fn = pycompat.sysbytes(filt.__name__)
640 640 sym = findsymbolicname(arg)
641 641 if not sym:
642 642 return _("incompatible use of template filter '%s'") % fn
643 643 return (_("template filter '%s' is not compatible with keyword '%s'")
644 644 % (fn, sym))
645 645
646 646 def _iteroverlaymaps(context, origmapping, newmappings):
647 647 """Generate combined mappings from the original mapping and an iterable
648 648 of partial mappings to override the original"""
649 649 for i, nm in enumerate(newmappings):
650 650 lm = context.overlaymap(origmapping, nm)
651 651 lm['index'] = i
652 652 yield lm
653 653
654 654 def _applymap(context, mapping, d, targ):
655 655 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
656 656 yield evalrawexp(context, lm, targ)
657 657
658 658 def runmap(context, mapping, data):
659 659 darg, targ = data
660 660 d = evalwrapped(context, mapping, darg)
661 661 return mappedgenerator(_applymap, args=(mapping, d, targ))
662 662
663 663 def runmember(context, mapping, data):
664 664 darg, memb = data
665 d = evalrawexp(context, mapping, darg)
665 d = evalwrapped(context, mapping, darg)
666 666 if util.safehasattr(d, 'tomap'):
667 667 lm = context.overlaymap(mapping, d.tomap())
668 668 return runsymbol(context, lm, memb)
669 669 if util.safehasattr(d, 'get'):
670 670 return getdictitem(d, memb)
671 671
672 672 sym = findsymbolicname(darg)
673 673 if sym:
674 674 raise error.ParseError(_("keyword '%s' has no member") % sym)
675 675 else:
676 676 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
677 677
678 678 def runnegate(context, mapping, data):
679 679 data = evalinteger(context, mapping, data,
680 680 _('negation needs an integer argument'))
681 681 return -data
682 682
683 683 def runarithmetic(context, mapping, data):
684 684 func, left, right = data
685 685 left = evalinteger(context, mapping, left,
686 686 _('arithmetic only defined on integers'))
687 687 right = evalinteger(context, mapping, right,
688 688 _('arithmetic only defined on integers'))
689 689 try:
690 690 return func(left, right)
691 691 except ZeroDivisionError:
692 692 raise error.Abort(_('division by zero is not defined'))
693 693
694 694 def getdictitem(dictarg, key):
695 695 val = dictarg.get(key)
696 696 if val is None:
697 697 return
698 698 return wraphybridvalue(dictarg, key, val)
699 699
700 700 def joinitems(itemiter, sep):
701 701 """Join items with the separator; Returns generator of bytes"""
702 702 first = True
703 703 for x in itemiter:
704 704 if first:
705 705 first = False
706 706 elif sep:
707 707 yield sep
708 708 yield x
General Comments 0
You need to be logged in to leave comments. Login now