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