##// END OF EJS Templates
templater: move getdictitem() to hybrid class...
Yuya Nishihara -
r38260:12b6ee9e 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 264 dictarg = evalwrapped(context, mapping, args[0])
265 if not util.safehasattr(dictarg, 'get'):
265 if not util.safehasattr(dictarg, 'getmember'):
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 return templateutil.getdictitem(dictarg, key)
270 return dictarg.getmember(context, mapping, 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,710 +1,715 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 def getmember(self, context, mapping, key):
132 # TODO: maybe split hybrid list/dict types?
133 if not util.safehasattr(self._values, 'get'):
134 raise error.ParseError(_('not a dictionary'))
135 return self._wrapvalue(key, self._values.get(key))
136
137 def _wrapvalue(self, key, val):
138 if val is None:
139 return
140 return wraphybridvalue(self, key, val)
141
131 142 def itermaps(self, context):
132 143 makemap = self._makemap
133 144 for x in self._values:
134 145 yield makemap(x)
135 146
136 147 def join(self, context, mapping, sep):
137 148 # TODO: switch gen to (context, mapping) API?
138 149 return joinitems((self._joinfmt(x) for x in self._values), sep)
139 150
140 151 def show(self, context, mapping):
141 152 # TODO: switch gen to (context, mapping) API?
142 153 gen = self._gen
143 154 if gen is None:
144 155 return self.join(context, mapping, ' ')
145 156 if callable(gen):
146 157 return gen()
147 158 return gen
148 159
149 160 def tovalue(self, context, mapping):
150 161 # TODO: return self._values and get rid of proxy methods
151 162 return self
152 163
153 164 def __contains__(self, x):
154 165 return x in self._values
155 166 def __getitem__(self, key):
156 167 return self._values[key]
157 168 def __len__(self):
158 169 return len(self._values)
159 170 def __iter__(self):
160 171 return iter(self._values)
161 172 def __getattr__(self, name):
162 173 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
163 174 r'itervalues', r'keys', r'values'):
164 175 raise AttributeError(name)
165 176 return getattr(self._values, name)
166 177
167 178 class mappable(wrapped):
168 179 """Wrapper for non-list/dict object to support map operation
169 180
170 181 This class allows us to handle both:
171 182 - "{manifest}"
172 183 - "{manifest % '{rev}:{node}'}"
173 184 - "{manifest.rev}"
174 185
175 186 Unlike a hybrid, this does not simulate the behavior of the underling
176 187 value.
177 188 """
178 189
179 190 def __init__(self, gen, key, value, makemap):
180 191 self._gen = gen # generator or function returning generator
181 192 self._key = key
182 193 self._value = value # may be generator of strings
183 194 self._makemap = makemap
184 195
185 196 def tomap(self):
186 197 return self._makemap(self._key)
187 198
188 199 def itermaps(self, context):
189 200 yield self.tomap()
190 201
191 202 def join(self, context, mapping, sep):
192 203 w = makewrapped(context, mapping, self._value)
193 204 return w.join(context, mapping, sep)
194 205
195 206 def show(self, context, mapping):
196 207 # TODO: switch gen to (context, mapping) API?
197 208 gen = self._gen
198 209 if gen is None:
199 210 return pycompat.bytestr(self._value)
200 211 if callable(gen):
201 212 return gen()
202 213 return gen
203 214
204 215 def tovalue(self, context, mapping):
205 216 return _unthunk(context, mapping, self._value)
206 217
207 218 class _mappingsequence(wrapped):
208 219 """Wrapper for sequence of template mappings
209 220
210 221 This represents an inner template structure (i.e. a list of dicts),
211 222 which can also be rendered by the specified named/literal template.
212 223
213 224 Template mappings may be nested.
214 225 """
215 226
216 227 def __init__(self, name=None, tmpl=None, sep=''):
217 228 if name is not None and tmpl is not None:
218 229 raise error.ProgrammingError('name and tmpl are mutually exclusive')
219 230 self._name = name
220 231 self._tmpl = tmpl
221 232 self._defaultsep = sep
222 233
223 234 def join(self, context, mapping, sep):
224 235 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
225 236 if self._name:
226 237 itemiter = (context.process(self._name, m) for m in mapsiter)
227 238 elif self._tmpl:
228 239 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
229 240 else:
230 241 raise error.ParseError(_('not displayable without template'))
231 242 return joinitems(itemiter, sep)
232 243
233 244 def show(self, context, mapping):
234 245 return self.join(context, mapping, self._defaultsep)
235 246
236 247 def tovalue(self, context, mapping):
237 248 knownres = context.knownresourcekeys()
238 249 items = []
239 250 for nm in self.itermaps(context):
240 251 # drop internal resources (recursively) which shouldn't be displayed
241 252 lm = context.overlaymap(mapping, nm)
242 253 items.append({k: unwrapvalue(context, lm, v)
243 254 for k, v in nm.iteritems() if k not in knownres})
244 255 return items
245 256
246 257 class mappinggenerator(_mappingsequence):
247 258 """Wrapper for generator of template mappings
248 259
249 260 The function ``make(context, *args)`` should return a generator of
250 261 mapping dicts.
251 262 """
252 263
253 264 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
254 265 super(mappinggenerator, self).__init__(name, tmpl, sep)
255 266 self._make = make
256 267 self._args = args
257 268
258 269 def itermaps(self, context):
259 270 return self._make(context, *self._args)
260 271
261 272 class mappinglist(_mappingsequence):
262 273 """Wrapper for list of template mappings"""
263 274
264 275 def __init__(self, mappings, name=None, tmpl=None, sep=''):
265 276 super(mappinglist, self).__init__(name, tmpl, sep)
266 277 self._mappings = mappings
267 278
268 279 def itermaps(self, context):
269 280 return iter(self._mappings)
270 281
271 282 class mappedgenerator(wrapped):
272 283 """Wrapper for generator of strings which acts as a list
273 284
274 285 The function ``make(context, *args)`` should return a generator of
275 286 byte strings, or a generator of (possibly nested) generators of byte
276 287 strings (i.e. a generator for a list of byte strings.)
277 288 """
278 289
279 290 def __init__(self, make, args=()):
280 291 self._make = make
281 292 self._args = args
282 293
283 294 def _gen(self, context):
284 295 return self._make(context, *self._args)
285 296
286 297 def itermaps(self, context):
287 298 raise error.ParseError(_('list of strings is not mappable'))
288 299
289 300 def join(self, context, mapping, sep):
290 301 return joinitems(self._gen(context), sep)
291 302
292 303 def show(self, context, mapping):
293 304 return self.join(context, mapping, '')
294 305
295 306 def tovalue(self, context, mapping):
296 307 return [stringify(context, mapping, x) for x in self._gen(context)]
297 308
298 309 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
299 310 """Wrap data to support both dict-like and string-like operations"""
300 311 prefmt = pycompat.identity
301 312 if fmt is None:
302 313 fmt = '%s=%s'
303 314 prefmt = pycompat.bytestr
304 315 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
305 316 lambda k: fmt % (prefmt(k), prefmt(data[k])))
306 317
307 318 def hybridlist(data, name, fmt=None, gen=None):
308 319 """Wrap data to support both list-like and string-like operations"""
309 320 prefmt = pycompat.identity
310 321 if fmt is None:
311 322 fmt = '%s'
312 323 prefmt = pycompat.bytestr
313 324 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
314 325
315 326 def unwraphybrid(context, mapping, thing):
316 327 """Return an object which can be stringified possibly by using a legacy
317 328 template"""
318 329 if not isinstance(thing, wrapped):
319 330 return thing
320 331 return thing.show(context, mapping)
321 332
322 333 def wraphybridvalue(container, key, value):
323 334 """Wrap an element of hybrid container to be mappable
324 335
325 336 The key is passed to the makemap function of the given container, which
326 337 should be an item generated by iter(container).
327 338 """
328 339 makemap = getattr(container, '_makemap', None)
329 340 if makemap is None:
330 341 return value
331 342 if util.safehasattr(value, '_makemap'):
332 343 # a nested hybrid list/dict, which has its own way of map operation
333 344 return value
334 345 return mappable(None, key, value, makemap)
335 346
336 347 def compatdict(context, mapping, name, data, key='key', value='value',
337 348 fmt=None, plural=None, separator=' '):
338 349 """Wrap data like hybriddict(), but also supports old-style list template
339 350
340 351 This exists for backward compatibility with the old-style template. Use
341 352 hybriddict() for new template keywords.
342 353 """
343 354 c = [{key: k, value: v} for k, v in data.iteritems()]
344 355 f = _showcompatlist(context, mapping, name, c, plural, separator)
345 356 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
346 357
347 358 def compatlist(context, mapping, name, data, element=None, fmt=None,
348 359 plural=None, separator=' '):
349 360 """Wrap data like hybridlist(), but also supports old-style list template
350 361
351 362 This exists for backward compatibility with the old-style template. Use
352 363 hybridlist() for new template keywords.
353 364 """
354 365 f = _showcompatlist(context, mapping, name, data, plural, separator)
355 366 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
356 367
357 368 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
358 369 """Return a generator that renders old-style list template
359 370
360 371 name is name of key in template map.
361 372 values is list of strings or dicts.
362 373 plural is plural of name, if not simply name + 's'.
363 374 separator is used to join values as a string
364 375
365 376 expansion works like this, given name 'foo'.
366 377
367 378 if values is empty, expand 'no_foos'.
368 379
369 380 if 'foo' not in template map, return values as a string,
370 381 joined by 'separator'.
371 382
372 383 expand 'start_foos'.
373 384
374 385 for each value, expand 'foo'. if 'last_foo' in template
375 386 map, expand it instead of 'foo' for last key.
376 387
377 388 expand 'end_foos'.
378 389 """
379 390 if not plural:
380 391 plural = name + 's'
381 392 if not values:
382 393 noname = 'no_' + plural
383 394 if context.preload(noname):
384 395 yield context.process(noname, mapping)
385 396 return
386 397 if not context.preload(name):
387 398 if isinstance(values[0], bytes):
388 399 yield separator.join(values)
389 400 else:
390 401 for v in values:
391 402 r = dict(v)
392 403 r.update(mapping)
393 404 yield r
394 405 return
395 406 startname = 'start_' + plural
396 407 if context.preload(startname):
397 408 yield context.process(startname, mapping)
398 409 def one(v, tag=name):
399 410 vmapping = {}
400 411 try:
401 412 vmapping.update(v)
402 413 # Python 2 raises ValueError if the type of v is wrong. Python
403 414 # 3 raises TypeError.
404 415 except (AttributeError, TypeError, ValueError):
405 416 try:
406 417 # Python 2 raises ValueError trying to destructure an e.g.
407 418 # bytes. Python 3 raises TypeError.
408 419 for a, b in v:
409 420 vmapping[a] = b
410 421 except (TypeError, ValueError):
411 422 vmapping[name] = v
412 423 vmapping = context.overlaymap(mapping, vmapping)
413 424 return context.process(tag, vmapping)
414 425 lastname = 'last_' + name
415 426 if context.preload(lastname):
416 427 last = values.pop()
417 428 else:
418 429 last = None
419 430 for v in values:
420 431 yield one(v)
421 432 if last is not None:
422 433 yield one(last, tag=lastname)
423 434 endname = 'end_' + plural
424 435 if context.preload(endname):
425 436 yield context.process(endname, mapping)
426 437
427 438 def flatten(context, mapping, thing):
428 439 """Yield a single stream from a possibly nested set of iterators"""
429 440 thing = unwraphybrid(context, mapping, thing)
430 441 if isinstance(thing, bytes):
431 442 yield thing
432 443 elif isinstance(thing, str):
433 444 # We can only hit this on Python 3, and it's here to guard
434 445 # against infinite recursion.
435 446 raise error.ProgrammingError('Mercurial IO including templates is done'
436 447 ' with bytes, not strings, got %r' % thing)
437 448 elif thing is None:
438 449 pass
439 450 elif not util.safehasattr(thing, '__iter__'):
440 451 yield pycompat.bytestr(thing)
441 452 else:
442 453 for i in thing:
443 454 i = unwraphybrid(context, mapping, i)
444 455 if isinstance(i, bytes):
445 456 yield i
446 457 elif i is None:
447 458 pass
448 459 elif not util.safehasattr(i, '__iter__'):
449 460 yield pycompat.bytestr(i)
450 461 else:
451 462 for j in flatten(context, mapping, i):
452 463 yield j
453 464
454 465 def stringify(context, mapping, thing):
455 466 """Turn values into bytes by converting into text and concatenating them"""
456 467 if isinstance(thing, bytes):
457 468 return thing # retain localstr to be round-tripped
458 469 return b''.join(flatten(context, mapping, thing))
459 470
460 471 def findsymbolicname(arg):
461 472 """Find symbolic name for the given compiled expression; returns None
462 473 if nothing found reliably"""
463 474 while True:
464 475 func, data = arg
465 476 if func is runsymbol:
466 477 return data
467 478 elif func is runfilter:
468 479 arg = data[0]
469 480 else:
470 481 return None
471 482
472 483 def _unthunk(context, mapping, thing):
473 484 """Evaluate a lazy byte string into value"""
474 485 if not isinstance(thing, types.GeneratorType):
475 486 return thing
476 487 return stringify(context, mapping, thing)
477 488
478 489 def evalrawexp(context, mapping, arg):
479 490 """Evaluate given argument as a bare template object which may require
480 491 further processing (such as folding generator of strings)"""
481 492 func, data = arg
482 493 return func(context, mapping, data)
483 494
484 495 def evalwrapped(context, mapping, arg):
485 496 """Evaluate given argument to wrapped object"""
486 497 thing = evalrawexp(context, mapping, arg)
487 498 return makewrapped(context, mapping, thing)
488 499
489 500 def makewrapped(context, mapping, thing):
490 501 """Lift object to a wrapped type"""
491 502 if isinstance(thing, wrapped):
492 503 return thing
493 504 thing = _unthunk(context, mapping, thing)
494 505 if isinstance(thing, bytes):
495 506 return wrappedbytes(thing)
496 507 return wrappedvalue(thing)
497 508
498 509 def evalfuncarg(context, mapping, arg):
499 510 """Evaluate given argument as value type"""
500 511 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
501 512
502 513 def unwrapvalue(context, mapping, thing):
503 514 """Move the inner value object out of the wrapper"""
504 515 if isinstance(thing, wrapped):
505 516 return thing.tovalue(context, mapping)
506 517 # evalrawexp() may return string, generator of strings or arbitrary object
507 518 # such as date tuple, but filter does not want generator.
508 519 return _unthunk(context, mapping, thing)
509 520
510 521 def evalboolean(context, mapping, arg):
511 522 """Evaluate given argument as boolean, but also takes boolean literals"""
512 523 func, data = arg
513 524 if func is runsymbol:
514 525 thing = func(context, mapping, data, default=None)
515 526 if thing is None:
516 527 # not a template keyword, takes as a boolean literal
517 528 thing = stringutil.parsebool(data)
518 529 else:
519 530 thing = func(context, mapping, data)
520 531 if isinstance(thing, wrapped):
521 532 thing = thing.tovalue(context, mapping)
522 533 if isinstance(thing, bool):
523 534 return thing
524 535 # other objects are evaluated as strings, which means 0 is True, but
525 536 # empty dict/list should be False as they are expected to be ''
526 537 return bool(stringify(context, mapping, thing))
527 538
528 539 def evaldate(context, mapping, arg, err=None):
529 540 """Evaluate given argument as a date tuple or a date string; returns
530 541 a (unixtime, offset) tuple"""
531 542 thing = evalrawexp(context, mapping, arg)
532 543 return unwrapdate(context, mapping, thing, err)
533 544
534 545 def unwrapdate(context, mapping, thing, err=None):
535 546 thing = unwrapvalue(context, mapping, thing)
536 547 try:
537 548 return dateutil.parsedate(thing)
538 549 except AttributeError:
539 550 raise error.ParseError(err or _('not a date tuple nor a string'))
540 551 except error.ParseError:
541 552 if not err:
542 553 raise
543 554 raise error.ParseError(err)
544 555
545 556 def evalinteger(context, mapping, arg, err=None):
546 557 thing = evalrawexp(context, mapping, arg)
547 558 return unwrapinteger(context, mapping, thing, err)
548 559
549 560 def unwrapinteger(context, mapping, thing, err=None):
550 561 thing = unwrapvalue(context, mapping, thing)
551 562 try:
552 563 return int(thing)
553 564 except (TypeError, ValueError):
554 565 raise error.ParseError(err or _('not an integer'))
555 566
556 567 def evalstring(context, mapping, arg):
557 568 return stringify(context, mapping, evalrawexp(context, mapping, arg))
558 569
559 570 def evalstringliteral(context, mapping, arg):
560 571 """Evaluate given argument as string template, but returns symbol name
561 572 if it is unknown"""
562 573 func, data = arg
563 574 if func is runsymbol:
564 575 thing = func(context, mapping, data, default=data)
565 576 else:
566 577 thing = func(context, mapping, data)
567 578 return stringify(context, mapping, thing)
568 579
569 580 _unwrapfuncbytype = {
570 581 None: unwrapvalue,
571 582 bytes: stringify,
572 583 date: unwrapdate,
573 584 int: unwrapinteger,
574 585 }
575 586
576 587 def unwrapastype(context, mapping, thing, typ):
577 588 """Move the inner value object out of the wrapper and coerce its type"""
578 589 try:
579 590 f = _unwrapfuncbytype[typ]
580 591 except KeyError:
581 592 raise error.ProgrammingError('invalid type specified: %r' % typ)
582 593 return f(context, mapping, thing)
583 594
584 595 def runinteger(context, mapping, data):
585 596 return int(data)
586 597
587 598 def runstring(context, mapping, data):
588 599 return data
589 600
590 601 def _recursivesymbolblocker(key):
591 602 def showrecursion(**args):
592 603 raise error.Abort(_("recursive reference '%s' in template") % key)
593 604 return showrecursion
594 605
595 606 def runsymbol(context, mapping, key, default=''):
596 607 v = context.symbol(mapping, key)
597 608 if v is None:
598 609 # put poison to cut recursion. we can't move this to parsing phase
599 610 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
600 611 safemapping = mapping.copy()
601 612 safemapping[key] = _recursivesymbolblocker(key)
602 613 try:
603 614 v = context.process(key, safemapping)
604 615 except TemplateNotFound:
605 616 v = default
606 617 if callable(v) and getattr(v, '_requires', None) is None:
607 618 # old templatekw: expand all keywords and resources
608 619 # (TODO: deprecate this after porting web template keywords to new API)
609 620 props = {k: context._resources.lookup(context, mapping, k)
610 621 for k in context._resources.knownkeys()}
611 622 # pass context to _showcompatlist() through templatekw._showlist()
612 623 props['templ'] = context
613 624 props.update(mapping)
614 625 return v(**pycompat.strkwargs(props))
615 626 if callable(v):
616 627 # new templatekw
617 628 try:
618 629 return v(context, mapping)
619 630 except ResourceUnavailable:
620 631 # unsupported keyword is mapped to empty just like unknown keyword
621 632 return None
622 633 return v
623 634
624 635 def runtemplate(context, mapping, template):
625 636 for arg in template:
626 637 yield evalrawexp(context, mapping, arg)
627 638
628 639 def runfilter(context, mapping, data):
629 640 arg, filt = data
630 641 thing = evalrawexp(context, mapping, arg)
631 642 intype = getattr(filt, '_intype', None)
632 643 try:
633 644 thing = unwrapastype(context, mapping, thing, intype)
634 645 return filt(thing)
635 646 except error.ParseError as e:
636 647 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
637 648
638 649 def _formatfiltererror(arg, filt):
639 650 fn = pycompat.sysbytes(filt.__name__)
640 651 sym = findsymbolicname(arg)
641 652 if not sym:
642 653 return _("incompatible use of template filter '%s'") % fn
643 654 return (_("template filter '%s' is not compatible with keyword '%s'")
644 655 % (fn, sym))
645 656
646 657 def _iteroverlaymaps(context, origmapping, newmappings):
647 658 """Generate combined mappings from the original mapping and an iterable
648 659 of partial mappings to override the original"""
649 660 for i, nm in enumerate(newmappings):
650 661 lm = context.overlaymap(origmapping, nm)
651 662 lm['index'] = i
652 663 yield lm
653 664
654 665 def _applymap(context, mapping, d, targ):
655 666 for lm in _iteroverlaymaps(context, mapping, d.itermaps(context)):
656 667 yield evalrawexp(context, lm, targ)
657 668
658 669 def runmap(context, mapping, data):
659 670 darg, targ = data
660 671 d = evalwrapped(context, mapping, darg)
661 672 return mappedgenerator(_applymap, args=(mapping, d, targ))
662 673
663 674 def runmember(context, mapping, data):
664 675 darg, memb = data
665 676 d = evalwrapped(context, mapping, darg)
666 677 if util.safehasattr(d, 'tomap'):
667 678 lm = context.overlaymap(mapping, d.tomap())
668 679 return runsymbol(context, lm, memb)
669 680 try:
670 if util.safehasattr(d, 'get'):
671 return getdictitem(d, memb)
681 if util.safehasattr(d, 'getmember'):
682 return d.getmember(context, mapping, memb)
672 683 raise error.ParseError
673 684 except error.ParseError:
674 685 sym = findsymbolicname(darg)
675 686 if sym:
676 687 raise error.ParseError(_("keyword '%s' has no member") % sym)
677 688 else:
678 689 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
679 690
680 691 def runnegate(context, mapping, data):
681 692 data = evalinteger(context, mapping, data,
682 693 _('negation needs an integer argument'))
683 694 return -data
684 695
685 696 def runarithmetic(context, mapping, data):
686 697 func, left, right = data
687 698 left = evalinteger(context, mapping, left,
688 699 _('arithmetic only defined on integers'))
689 700 right = evalinteger(context, mapping, right,
690 701 _('arithmetic only defined on integers'))
691 702 try:
692 703 return func(left, right)
693 704 except ZeroDivisionError:
694 705 raise error.Abort(_('division by zero is not defined'))
695 706
696 def getdictitem(dictarg, key):
697 val = dictarg.get(key)
698 if val is None:
699 return
700 return wraphybridvalue(dictarg, key, val)
701
702 707 def joinitems(itemiter, sep):
703 708 """Join items with the separator; Returns generator of bytes"""
704 709 first = True
705 710 for x in itemiter:
706 711 if first:
707 712 first = False
708 713 elif sep:
709 714 yield sep
710 715 yield x
General Comments 0
You need to be logged in to leave comments. Login now