##// END OF EJS Templates
templater: remember cache key of evaluated revset...
Yuya Nishihara -
r45082:1f81f680 default
parent child Browse files
Show More
@@ -1,880 +1,882
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 diffutil,
20 20 encoding,
21 21 error,
22 22 minirst,
23 23 obsutil,
24 24 pycompat,
25 25 registrar,
26 26 revset as revsetmod,
27 27 revsetlang,
28 28 scmutil,
29 29 templatefilters,
30 30 templatekw,
31 31 templateutil,
32 32 util,
33 33 )
34 34 from .utils import (
35 35 dateutil,
36 36 stringutil,
37 37 )
38 38
39 39 evalrawexp = templateutil.evalrawexp
40 40 evalwrapped = templateutil.evalwrapped
41 41 evalfuncarg = templateutil.evalfuncarg
42 42 evalboolean = templateutil.evalboolean
43 43 evaldate = templateutil.evaldate
44 44 evalinteger = templateutil.evalinteger
45 45 evalstring = templateutil.evalstring
46 46 evalstringliteral = templateutil.evalstringliteral
47 47
48 48 # dict of template built-in functions
49 49 funcs = {}
50 50 templatefunc = registrar.templatefunc(funcs)
51 51
52 52
53 53 @templatefunc(b'date(date[, fmt])')
54 54 def date(context, mapping, args):
55 55 """Format a date. See :hg:`help dates` for formatting
56 56 strings. The default is a Unix date format, including the timezone:
57 57 "Mon Sep 04 15:13:13 2006 0700"."""
58 58 if not (1 <= len(args) <= 2):
59 59 # i18n: "date" is a keyword
60 60 raise error.ParseError(_(b"date expects one or two arguments"))
61 61
62 62 date = evaldate(
63 63 context,
64 64 mapping,
65 65 args[0],
66 66 # i18n: "date" is a keyword
67 67 _(b"date expects a date information"),
68 68 )
69 69 fmt = None
70 70 if len(args) == 2:
71 71 fmt = evalstring(context, mapping, args[1])
72 72 if fmt is None:
73 73 return dateutil.datestr(date)
74 74 else:
75 75 return dateutil.datestr(date, fmt)
76 76
77 77
78 78 @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs')
79 79 def dict_(context, mapping, args):
80 80 """Construct a dict from key-value pairs. A key may be omitted if
81 81 a value expression can provide an unambiguous name."""
82 82 data = util.sortdict()
83 83
84 84 for v in args[b'args']:
85 85 k = templateutil.findsymbolicname(v)
86 86 if not k:
87 87 raise error.ParseError(_(b'dict key cannot be inferred'))
88 88 if k in data or k in args[b'kwargs']:
89 89 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k)
90 90 data[k] = evalfuncarg(context, mapping, v)
91 91
92 92 data.update(
93 93 (k, evalfuncarg(context, mapping, v))
94 94 for k, v in pycompat.iteritems(args[b'kwargs'])
95 95 )
96 96 return templateutil.hybriddict(data)
97 97
98 98
99 99 @templatefunc(
100 100 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'}
101 101 )
102 102 def diff(context, mapping, args):
103 103 """Show a diff, optionally
104 104 specifying files to include or exclude."""
105 105 if len(args) > 2:
106 106 # i18n: "diff" is a keyword
107 107 raise error.ParseError(_(b"diff expects zero, one, or two arguments"))
108 108
109 109 def getpatterns(i):
110 110 if i < len(args):
111 111 s = evalstring(context, mapping, args[i]).strip()
112 112 if s:
113 113 return [s]
114 114 return []
115 115
116 116 ctx = context.resource(mapping, b'ctx')
117 117 ui = context.resource(mapping, b'ui')
118 118 diffopts = diffutil.diffallopts(ui)
119 119 chunks = ctx.diff(
120 120 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts
121 121 )
122 122
123 123 return b''.join(chunks)
124 124
125 125
126 126 @templatefunc(
127 127 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'}
128 128 )
129 129 def extdata(context, mapping, args):
130 130 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
131 131 if b'source' not in args:
132 132 # i18n: "extdata" is a keyword
133 133 raise error.ParseError(_(b'extdata expects one argument'))
134 134
135 135 source = evalstring(context, mapping, args[b'source'])
136 136 if not source:
137 137 sym = templateutil.findsymbolicname(args[b'source'])
138 138 if sym:
139 139 raise error.ParseError(
140 140 _(b'empty data source specified'),
141 141 hint=_(b"did you mean extdata('%s')?") % sym,
142 142 )
143 143 else:
144 144 raise error.ParseError(_(b'empty data source specified'))
145 145 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {})
146 146 ctx = context.resource(mapping, b'ctx')
147 147 if source in cache:
148 148 data = cache[source]
149 149 else:
150 150 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
151 151 return data.get(ctx.rev(), b'')
152 152
153 153
154 154 @templatefunc(b'files(pattern)', requires={b'ctx'})
155 155 def files(context, mapping, args):
156 156 """All files of the current changeset matching the pattern. See
157 157 :hg:`help patterns`."""
158 158 if not len(args) == 1:
159 159 # i18n: "files" is a keyword
160 160 raise error.ParseError(_(b"files expects one argument"))
161 161
162 162 raw = evalstring(context, mapping, args[0])
163 163 ctx = context.resource(mapping, b'ctx')
164 164 m = ctx.match([raw])
165 165 files = list(ctx.matches(m))
166 166 return templateutil.compatfileslist(context, mapping, b"file", files)
167 167
168 168
169 169 @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])')
170 170 def fill(context, mapping, args):
171 171 """Fill many
172 172 paragraphs with optional indentation. See the "fill" filter."""
173 173 if not (1 <= len(args) <= 4):
174 174 # i18n: "fill" is a keyword
175 175 raise error.ParseError(_(b"fill expects one to four arguments"))
176 176
177 177 text = evalstring(context, mapping, args[0])
178 178 width = 76
179 179 initindent = b''
180 180 hangindent = b''
181 181 if 2 <= len(args) <= 4:
182 182 width = evalinteger(
183 183 context,
184 184 mapping,
185 185 args[1],
186 186 # i18n: "fill" is a keyword
187 187 _(b"fill expects an integer width"),
188 188 )
189 189 try:
190 190 initindent = evalstring(context, mapping, args[2])
191 191 hangindent = evalstring(context, mapping, args[3])
192 192 except IndexError:
193 193 pass
194 194
195 195 return templatefilters.fill(text, width, initindent, hangindent)
196 196
197 197
198 198 @templatefunc(b'filter(iterable[, expr])')
199 199 def filter_(context, mapping, args):
200 200 """Remove empty elements from a list or a dict. If expr specified, it's
201 201 applied to each element to test emptiness."""
202 202 if not (1 <= len(args) <= 2):
203 203 # i18n: "filter" is a keyword
204 204 raise error.ParseError(_(b"filter expects one or two arguments"))
205 205 iterable = evalwrapped(context, mapping, args[0])
206 206 if len(args) == 1:
207 207
208 208 def select(w):
209 209 return w.tobool(context, mapping)
210 210
211 211 else:
212 212
213 213 def select(w):
214 214 if not isinstance(w, templateutil.mappable):
215 215 raise error.ParseError(_(b"not filterable by expression"))
216 216 lm = context.overlaymap(mapping, w.tomap(context))
217 217 return evalboolean(context, lm, args[1])
218 218
219 219 return iterable.filter(context, mapping, select)
220 220
221 221
222 222 @templatefunc(b'formatnode(node)', requires={b'ui'})
223 223 def formatnode(context, mapping, args):
224 224 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
225 225 if len(args) != 1:
226 226 # i18n: "formatnode" is a keyword
227 227 raise error.ParseError(_(b"formatnode expects one argument"))
228 228
229 229 ui = context.resource(mapping, b'ui')
230 230 node = evalstring(context, mapping, args[0])
231 231 if ui.debugflag:
232 232 return node
233 233 return templatefilters.short(node)
234 234
235 235
236 236 @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'})
237 237 def mailmap(context, mapping, args):
238 238 """Return the author, updated according to the value
239 239 set in the .mailmap file"""
240 240 if len(args) != 1:
241 241 raise error.ParseError(_(b"mailmap expects one argument"))
242 242
243 243 author = evalstring(context, mapping, args[0])
244 244
245 245 cache = context.resource(mapping, b'cache')
246 246 repo = context.resource(mapping, b'repo')
247 247
248 248 if b'mailmap' not in cache:
249 249 data = repo.wvfs.tryread(b'.mailmap')
250 250 cache[b'mailmap'] = stringutil.parsemailmap(data)
251 251
252 252 return stringutil.mapname(cache[b'mailmap'], author)
253 253
254 254
255 255 @templatefunc(
256 256 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
257 257 argspec=b'text width fillchar left truncate',
258 258 )
259 259 def pad(context, mapping, args):
260 260 """Pad text with a
261 261 fill character."""
262 262 if b'text' not in args or b'width' not in args:
263 263 # i18n: "pad" is a keyword
264 264 raise error.ParseError(_(b"pad() expects two to four arguments"))
265 265
266 266 width = evalinteger(
267 267 context,
268 268 mapping,
269 269 args[b'width'],
270 270 # i18n: "pad" is a keyword
271 271 _(b"pad() expects an integer width"),
272 272 )
273 273
274 274 text = evalstring(context, mapping, args[b'text'])
275 275
276 276 truncate = False
277 277 left = False
278 278 fillchar = b' '
279 279 if b'fillchar' in args:
280 280 fillchar = evalstring(context, mapping, args[b'fillchar'])
281 281 if len(color.stripeffects(fillchar)) != 1:
282 282 # i18n: "pad" is a keyword
283 283 raise error.ParseError(_(b"pad() expects a single fill character"))
284 284 if b'left' in args:
285 285 left = evalboolean(context, mapping, args[b'left'])
286 286 if b'truncate' in args:
287 287 truncate = evalboolean(context, mapping, args[b'truncate'])
288 288
289 289 fillwidth = width - encoding.colwidth(color.stripeffects(text))
290 290 if fillwidth < 0 and truncate:
291 291 return encoding.trim(color.stripeffects(text), width, leftside=left)
292 292 if fillwidth <= 0:
293 293 return text
294 294 if left:
295 295 return fillchar * fillwidth + text
296 296 else:
297 297 return text + fillchar * fillwidth
298 298
299 299
300 300 @templatefunc(b'indent(text, indentchars[, firstline])')
301 301 def indent(context, mapping, args):
302 302 """Indents all non-empty lines
303 303 with the characters given in the indentchars string. An optional
304 304 third parameter will override the indent for the first line only
305 305 if present."""
306 306 if not (2 <= len(args) <= 3):
307 307 # i18n: "indent" is a keyword
308 308 raise error.ParseError(_(b"indent() expects two or three arguments"))
309 309
310 310 text = evalstring(context, mapping, args[0])
311 311 indent = evalstring(context, mapping, args[1])
312 312
313 313 firstline = indent
314 314 if len(args) == 3:
315 315 firstline = evalstring(context, mapping, args[2])
316 316
317 317 return templatefilters.indent(text, indent, firstline=firstline)
318 318
319 319
320 320 @templatefunc(b'get(dict, key)')
321 321 def get(context, mapping, args):
322 322 """Get an attribute/key from an object. Some keywords
323 323 are complex types. This function allows you to obtain the value of an
324 324 attribute on these types."""
325 325 if len(args) != 2:
326 326 # i18n: "get" is a keyword
327 327 raise error.ParseError(_(b"get() expects two arguments"))
328 328
329 329 dictarg = evalwrapped(context, mapping, args[0])
330 330 key = evalrawexp(context, mapping, args[1])
331 331 try:
332 332 return dictarg.getmember(context, mapping, key)
333 333 except error.ParseError as err:
334 334 # i18n: "get" is a keyword
335 335 hint = _(b"get() expects a dict as first argument")
336 336 raise error.ParseError(bytes(err), hint=hint)
337 337
338 338
339 339 @templatefunc(b'config(section, name[, default])', requires={b'ui'})
340 340 def config(context, mapping, args):
341 341 """Returns the requested hgrc config option as a string."""
342 342 fn = context.resource(mapping, b'ui').config
343 343 return _config(context, mapping, args, fn, evalstring)
344 344
345 345
346 346 @templatefunc(b'configbool(section, name[, default])', requires={b'ui'})
347 347 def configbool(context, mapping, args):
348 348 """Returns the requested hgrc config option as a boolean."""
349 349 fn = context.resource(mapping, b'ui').configbool
350 350 return _config(context, mapping, args, fn, evalboolean)
351 351
352 352
353 353 @templatefunc(b'configint(section, name[, default])', requires={b'ui'})
354 354 def configint(context, mapping, args):
355 355 """Returns the requested hgrc config option as an integer."""
356 356 fn = context.resource(mapping, b'ui').configint
357 357 return _config(context, mapping, args, fn, evalinteger)
358 358
359 359
360 360 def _config(context, mapping, args, configfn, defaultfn):
361 361 if not (2 <= len(args) <= 3):
362 362 raise error.ParseError(_(b"config expects two or three arguments"))
363 363
364 364 # The config option can come from any section, though we specifically
365 365 # reserve the [templateconfig] section for dynamically defining options
366 366 # for this function without also requiring an extension.
367 367 section = evalstringliteral(context, mapping, args[0])
368 368 name = evalstringliteral(context, mapping, args[1])
369 369 if len(args) == 3:
370 370 default = defaultfn(context, mapping, args[2])
371 371 return configfn(section, name, default)
372 372 else:
373 373 return configfn(section, name)
374 374
375 375
376 376 @templatefunc(b'if(expr, then[, else])')
377 377 def if_(context, mapping, args):
378 378 """Conditionally execute based on the result of
379 379 an expression."""
380 380 if not (2 <= len(args) <= 3):
381 381 # i18n: "if" is a keyword
382 382 raise error.ParseError(_(b"if expects two or three arguments"))
383 383
384 384 test = evalboolean(context, mapping, args[0])
385 385 if test:
386 386 return evalrawexp(context, mapping, args[1])
387 387 elif len(args) == 3:
388 388 return evalrawexp(context, mapping, args[2])
389 389
390 390
391 391 @templatefunc(b'ifcontains(needle, haystack, then[, else])')
392 392 def ifcontains(context, mapping, args):
393 393 """Conditionally execute based
394 394 on whether the item "needle" is in "haystack"."""
395 395 if not (3 <= len(args) <= 4):
396 396 # i18n: "ifcontains" is a keyword
397 397 raise error.ParseError(_(b"ifcontains expects three or four arguments"))
398 398
399 399 haystack = evalwrapped(context, mapping, args[1])
400 400 try:
401 401 needle = evalrawexp(context, mapping, args[0])
402 402 found = haystack.contains(context, mapping, needle)
403 403 except error.ParseError:
404 404 found = False
405 405
406 406 if found:
407 407 return evalrawexp(context, mapping, args[2])
408 408 elif len(args) == 4:
409 409 return evalrawexp(context, mapping, args[3])
410 410
411 411
412 412 @templatefunc(b'ifeq(expr1, expr2, then[, else])')
413 413 def ifeq(context, mapping, args):
414 414 """Conditionally execute based on
415 415 whether 2 items are equivalent."""
416 416 if not (3 <= len(args) <= 4):
417 417 # i18n: "ifeq" is a keyword
418 418 raise error.ParseError(_(b"ifeq expects three or four arguments"))
419 419
420 420 test = evalstring(context, mapping, args[0])
421 421 match = evalstring(context, mapping, args[1])
422 422 if test == match:
423 423 return evalrawexp(context, mapping, args[2])
424 424 elif len(args) == 4:
425 425 return evalrawexp(context, mapping, args[3])
426 426
427 427
428 428 @templatefunc(b'join(list, sep)')
429 429 def join(context, mapping, args):
430 430 """Join items in a list with a delimiter."""
431 431 if not (1 <= len(args) <= 2):
432 432 # i18n: "join" is a keyword
433 433 raise error.ParseError(_(b"join expects one or two arguments"))
434 434
435 435 joinset = evalwrapped(context, mapping, args[0])
436 436 joiner = b" "
437 437 if len(args) > 1:
438 438 joiner = evalstring(context, mapping, args[1])
439 439 return joinset.join(context, mapping, joiner)
440 440
441 441
442 442 @templatefunc(b'label(label, expr)', requires={b'ui'})
443 443 def label(context, mapping, args):
444 444 """Apply a label to generated content. Content with
445 445 a label applied can result in additional post-processing, such as
446 446 automatic colorization."""
447 447 if len(args) != 2:
448 448 # i18n: "label" is a keyword
449 449 raise error.ParseError(_(b"label expects two arguments"))
450 450
451 451 ui = context.resource(mapping, b'ui')
452 452 thing = evalstring(context, mapping, args[1])
453 453 # preserve unknown symbol as literal so effects like 'red', 'bold',
454 454 # etc. don't need to be quoted
455 455 label = evalstringliteral(context, mapping, args[0])
456 456
457 457 return ui.label(thing, label)
458 458
459 459
460 460 @templatefunc(b'latesttag([pattern])')
461 461 def latesttag(context, mapping, args):
462 462 """The global tags matching the given pattern on the
463 463 most recent globally tagged ancestor of this changeset.
464 464 If no such tags exist, the "{tag}" template resolves to
465 465 the string "null". See :hg:`help revisions.patterns` for the pattern
466 466 syntax.
467 467 """
468 468 if len(args) > 1:
469 469 # i18n: "latesttag" is a keyword
470 470 raise error.ParseError(_(b"latesttag expects at most one argument"))
471 471
472 472 pattern = None
473 473 if len(args) == 1:
474 474 pattern = evalstring(context, mapping, args[0])
475 475 return templatekw.showlatesttags(context, mapping, pattern)
476 476
477 477
478 478 @templatefunc(b'localdate(date[, tz])')
479 479 def localdate(context, mapping, args):
480 480 """Converts a date to the specified timezone.
481 481 The default is local date."""
482 482 if not (1 <= len(args) <= 2):
483 483 # i18n: "localdate" is a keyword
484 484 raise error.ParseError(_(b"localdate expects one or two arguments"))
485 485
486 486 date = evaldate(
487 487 context,
488 488 mapping,
489 489 args[0],
490 490 # i18n: "localdate" is a keyword
491 491 _(b"localdate expects a date information"),
492 492 )
493 493 if len(args) >= 2:
494 494 tzoffset = None
495 495 tz = evalfuncarg(context, mapping, args[1])
496 496 if isinstance(tz, bytes):
497 497 tzoffset, remainder = dateutil.parsetimezone(tz)
498 498 if remainder:
499 499 tzoffset = None
500 500 if tzoffset is None:
501 501 try:
502 502 tzoffset = int(tz)
503 503 except (TypeError, ValueError):
504 504 # i18n: "localdate" is a keyword
505 505 raise error.ParseError(_(b"localdate expects a timezone"))
506 506 else:
507 507 tzoffset = dateutil.makedate()[1]
508 508 return templateutil.date((date[0], tzoffset))
509 509
510 510
511 511 @templatefunc(b'max(iterable)')
512 512 def max_(context, mapping, args, **kwargs):
513 513 """Return the max of an iterable"""
514 514 if len(args) != 1:
515 515 # i18n: "max" is a keyword
516 516 raise error.ParseError(_(b"max expects one argument"))
517 517
518 518 iterable = evalwrapped(context, mapping, args[0])
519 519 try:
520 520 return iterable.getmax(context, mapping)
521 521 except error.ParseError as err:
522 522 # i18n: "max" is a keyword
523 523 hint = _(b"max first argument should be an iterable")
524 524 raise error.ParseError(bytes(err), hint=hint)
525 525
526 526
527 527 @templatefunc(b'min(iterable)')
528 528 def min_(context, mapping, args, **kwargs):
529 529 """Return the min of an iterable"""
530 530 if len(args) != 1:
531 531 # i18n: "min" is a keyword
532 532 raise error.ParseError(_(b"min expects one argument"))
533 533
534 534 iterable = evalwrapped(context, mapping, args[0])
535 535 try:
536 536 return iterable.getmin(context, mapping)
537 537 except error.ParseError as err:
538 538 # i18n: "min" is a keyword
539 539 hint = _(b"min first argument should be an iterable")
540 540 raise error.ParseError(bytes(err), hint=hint)
541 541
542 542
543 543 @templatefunc(b'mod(a, b)')
544 544 def mod(context, mapping, args):
545 545 """Calculate a mod b such that a / b + a mod b == a"""
546 546 if not len(args) == 2:
547 547 # i18n: "mod" is a keyword
548 548 raise error.ParseError(_(b"mod expects two arguments"))
549 549
550 550 func = lambda a, b: a % b
551 551 return templateutil.runarithmetic(
552 552 context, mapping, (func, args[0], args[1])
553 553 )
554 554
555 555
556 556 @templatefunc(b'obsfateoperations(markers)')
557 557 def obsfateoperations(context, mapping, args):
558 558 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
559 559 if len(args) != 1:
560 560 # i18n: "obsfateoperations" is a keyword
561 561 raise error.ParseError(_(b"obsfateoperations expects one argument"))
562 562
563 563 markers = evalfuncarg(context, mapping, args[0])
564 564
565 565 try:
566 566 data = obsutil.markersoperations(markers)
567 567 return templateutil.hybridlist(data, name=b'operation')
568 568 except (TypeError, KeyError):
569 569 # i18n: "obsfateoperations" is a keyword
570 570 errmsg = _(b"obsfateoperations first argument should be an iterable")
571 571 raise error.ParseError(errmsg)
572 572
573 573
574 574 @templatefunc(b'obsfatedate(markers)')
575 575 def obsfatedate(context, mapping, args):
576 576 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
577 577 if len(args) != 1:
578 578 # i18n: "obsfatedate" is a keyword
579 579 raise error.ParseError(_(b"obsfatedate expects one argument"))
580 580
581 581 markers = evalfuncarg(context, mapping, args[0])
582 582
583 583 try:
584 584 # TODO: maybe this has to be a wrapped list of date wrappers?
585 585 data = obsutil.markersdates(markers)
586 586 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d')
587 587 except (TypeError, KeyError):
588 588 # i18n: "obsfatedate" is a keyword
589 589 errmsg = _(b"obsfatedate first argument should be an iterable")
590 590 raise error.ParseError(errmsg)
591 591
592 592
593 593 @templatefunc(b'obsfateusers(markers)')
594 594 def obsfateusers(context, mapping, args):
595 595 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
596 596 if len(args) != 1:
597 597 # i18n: "obsfateusers" is a keyword
598 598 raise error.ParseError(_(b"obsfateusers expects one argument"))
599 599
600 600 markers = evalfuncarg(context, mapping, args[0])
601 601
602 602 try:
603 603 data = obsutil.markersusers(markers)
604 604 return templateutil.hybridlist(data, name=b'user')
605 605 except (TypeError, KeyError, ValueError):
606 606 # i18n: "obsfateusers" is a keyword
607 607 msg = _(
608 608 b"obsfateusers first argument should be an iterable of "
609 609 b"obsmakers"
610 610 )
611 611 raise error.ParseError(msg)
612 612
613 613
614 614 @templatefunc(b'obsfateverb(successors, markers)')
615 615 def obsfateverb(context, mapping, args):
616 616 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
617 617 if len(args) != 2:
618 618 # i18n: "obsfateverb" is a keyword
619 619 raise error.ParseError(_(b"obsfateverb expects two arguments"))
620 620
621 621 successors = evalfuncarg(context, mapping, args[0])
622 622 markers = evalfuncarg(context, mapping, args[1])
623 623
624 624 try:
625 625 return obsutil.obsfateverb(successors, markers)
626 626 except TypeError:
627 627 # i18n: "obsfateverb" is a keyword
628 628 errmsg = _(b"obsfateverb first argument should be countable")
629 629 raise error.ParseError(errmsg)
630 630
631 631
632 632 @templatefunc(b'relpath(path)', requires={b'repo'})
633 633 def relpath(context, mapping, args):
634 634 """Convert a repository-absolute path into a filesystem path relative to
635 635 the current working directory."""
636 636 if len(args) != 1:
637 637 # i18n: "relpath" is a keyword
638 638 raise error.ParseError(_(b"relpath expects one argument"))
639 639
640 640 repo = context.resource(mapping, b'repo')
641 641 path = evalstring(context, mapping, args[0])
642 642 return repo.pathto(path)
643 643
644 644
645 645 @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'})
646 646 def revset(context, mapping, args):
647 647 """Execute a revision set query. See
648 648 :hg:`help revset`."""
649 649 if not len(args) > 0:
650 650 # i18n: "revset" is a keyword
651 651 raise error.ParseError(_(b"revset expects one or more arguments"))
652 652
653 653 raw = evalstring(context, mapping, args[0])
654 654 repo = context.resource(mapping, b'repo')
655 655
656 656 def query(expr):
657 657 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
658 658 return m(repo)
659 659
660 660 if len(args) > 1:
661 key = None # dynamically-created revs shouldn't be cached
661 662 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
662 663 revs = query(revsetlang.formatspec(raw, *formatargs))
663 664 else:
664 665 cache = context.resource(mapping, b'cache')
665 666 revsetcache = cache.setdefault(b"revsetcache", {})
666 if raw in revsetcache:
667 revs = revsetcache[raw]
667 key = raw
668 if key in revsetcache:
669 revs = revsetcache[key]
668 670 else:
669 671 revs = query(raw)
670 revsetcache[raw] = revs
671 return templateutil.revslist(repo, revs, name=b'revision')
672 revsetcache[key] = revs
673 return templateutil.revslist(repo, revs, name=b'revision', cachekey=key)
672 674
673 675
674 676 @templatefunc(b'rstdoc(text, style)')
675 677 def rstdoc(context, mapping, args):
676 678 """Format reStructuredText."""
677 679 if len(args) != 2:
678 680 # i18n: "rstdoc" is a keyword
679 681 raise error.ParseError(_(b"rstdoc expects two arguments"))
680 682
681 683 text = evalstring(context, mapping, args[0])
682 684 style = evalstring(context, mapping, args[1])
683 685
684 686 return minirst.format(text, style=style, keep=[b'verbose'])
685 687
686 688
687 689 @templatefunc(b'search(pattern, text)')
688 690 def search(context, mapping, args):
689 691 """Look for the first text matching the regular expression pattern.
690 692 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
691 693 if len(args) != 2:
692 694 # i18n: "search" is a keyword
693 695 raise error.ParseError(_(b'search expects two arguments'))
694 696
695 697 pat = evalstring(context, mapping, args[0])
696 698 src = evalstring(context, mapping, args[1])
697 699 try:
698 700 patre = re.compile(pat)
699 701 except re.error:
700 702 # i18n: "search" is a keyword
701 703 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
702 704 # named groups shouldn't shadow *reserved* resource keywords
703 705 badgroups = context.knownresourcekeys() & set(
704 706 pycompat.byteskwargs(patre.groupindex)
705 707 )
706 708 if badgroups:
707 709 raise error.ParseError(
708 710 # i18n: "search" is a keyword
709 711 _(b'invalid group %(group)s in search pattern: %(pat)s')
710 712 % {
711 713 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)),
712 714 b'pat': pat,
713 715 }
714 716 )
715 717
716 718 match = patre.search(src)
717 719 if not match:
718 720 return templateutil.mappingnone()
719 721
720 722 lm = {b'0': match.group(0)}
721 723 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
722 724 lm.update(pycompat.byteskwargs(match.groupdict()))
723 725 return templateutil.mappingdict(lm, tmpl=b'{0}')
724 726
725 727
726 728 @templatefunc(b'separate(sep, args...)', argspec=b'sep *args')
727 729 def separate(context, mapping, args):
728 730 """Add a separator between non-empty arguments."""
729 731 if b'sep' not in args:
730 732 # i18n: "separate" is a keyword
731 733 raise error.ParseError(_(b"separate expects at least one argument"))
732 734
733 735 sep = evalstring(context, mapping, args[b'sep'])
734 736 first = True
735 737 for arg in args[b'args']:
736 738 argstr = evalstring(context, mapping, arg)
737 739 if not argstr:
738 740 continue
739 741 if first:
740 742 first = False
741 743 else:
742 744 yield sep
743 745 yield argstr
744 746
745 747
746 748 @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'})
747 749 def shortest(context, mapping, args):
748 750 """Obtain the shortest representation of
749 751 a node."""
750 752 if not (1 <= len(args) <= 2):
751 753 # i18n: "shortest" is a keyword
752 754 raise error.ParseError(_(b"shortest() expects one or two arguments"))
753 755
754 756 hexnode = evalstring(context, mapping, args[0])
755 757
756 758 minlength = 4
757 759 if len(args) > 1:
758 760 minlength = evalinteger(
759 761 context,
760 762 mapping,
761 763 args[1],
762 764 # i18n: "shortest" is a keyword
763 765 _(b"shortest() expects an integer minlength"),
764 766 )
765 767
766 768 repo = context.resource(mapping, b'repo')
767 769 if len(hexnode) > 40:
768 770 return hexnode
769 771 elif len(hexnode) == 40:
770 772 try:
771 773 node = bin(hexnode)
772 774 except TypeError:
773 775 return hexnode
774 776 else:
775 777 try:
776 778 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
777 779 except error.WdirUnsupported:
778 780 node = wdirid
779 781 except error.LookupError:
780 782 return hexnode
781 783 if not node:
782 784 return hexnode
783 785 cache = context.resource(mapping, b'cache')
784 786 try:
785 787 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
786 788 except error.RepoLookupError:
787 789 return hexnode
788 790
789 791
790 792 @templatefunc(b'strip(text[, chars])')
791 793 def strip(context, mapping, args):
792 794 """Strip characters from a string. By default,
793 795 strips all leading and trailing whitespace."""
794 796 if not (1 <= len(args) <= 2):
795 797 # i18n: "strip" is a keyword
796 798 raise error.ParseError(_(b"strip expects one or two arguments"))
797 799
798 800 text = evalstring(context, mapping, args[0])
799 801 if len(args) == 2:
800 802 chars = evalstring(context, mapping, args[1])
801 803 return text.strip(chars)
802 804 return text.strip()
803 805
804 806
805 807 @templatefunc(b'sub(pattern, replacement, expression)')
806 808 def sub(context, mapping, args):
807 809 """Perform text substitution
808 810 using regular expressions."""
809 811 if len(args) != 3:
810 812 # i18n: "sub" is a keyword
811 813 raise error.ParseError(_(b"sub expects three arguments"))
812 814
813 815 pat = evalstring(context, mapping, args[0])
814 816 rpl = evalstring(context, mapping, args[1])
815 817 src = evalstring(context, mapping, args[2])
816 818 try:
817 819 patre = re.compile(pat)
818 820 except re.error:
819 821 # i18n: "sub" is a keyword
820 822 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat)
821 823 try:
822 824 yield patre.sub(rpl, src)
823 825 except re.error:
824 826 # i18n: "sub" is a keyword
825 827 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl)
826 828
827 829
828 830 @templatefunc(b'startswith(pattern, text)')
829 831 def startswith(context, mapping, args):
830 832 """Returns the value from the "text" argument
831 833 if it begins with the content from the "pattern" argument."""
832 834 if len(args) != 2:
833 835 # i18n: "startswith" is a keyword
834 836 raise error.ParseError(_(b"startswith expects two arguments"))
835 837
836 838 patn = evalstring(context, mapping, args[0])
837 839 text = evalstring(context, mapping, args[1])
838 840 if text.startswith(patn):
839 841 return text
840 842 return b''
841 843
842 844
843 845 @templatefunc(b'word(number, text[, separator])')
844 846 def word(context, mapping, args):
845 847 """Return the nth word from a string."""
846 848 if not (2 <= len(args) <= 3):
847 849 # i18n: "word" is a keyword
848 850 raise error.ParseError(
849 851 _(b"word expects two or three arguments, got %d") % len(args)
850 852 )
851 853
852 854 num = evalinteger(
853 855 context,
854 856 mapping,
855 857 args[0],
856 858 # i18n: "word" is a keyword
857 859 _(b"word expects an integer index"),
858 860 )
859 861 text = evalstring(context, mapping, args[1])
860 862 if len(args) == 3:
861 863 splitter = evalstring(context, mapping, args[2])
862 864 else:
863 865 splitter = None
864 866
865 867 tokens = text.split(splitter)
866 868 if num >= len(tokens) or num < -len(tokens):
867 869 return b''
868 870 else:
869 871 return tokens[num]
870 872
871 873
872 874 def loadfunction(ui, extname, registrarobj):
873 875 """Load template function from specified registrarobj
874 876 """
875 877 for name, func in pycompat.iteritems(registrarobj._table):
876 878 funcs[name] = func
877 879
878 880
879 881 # tell hggettext to extract docstrings from these functions:
880 882 i18nfunctions = funcs.values()
@@ -1,1165 +1,1170
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 .pycompat import getattr
15 15 from . import (
16 16 error,
17 17 pycompat,
18 18 smartset,
19 19 util,
20 20 )
21 21 from .utils import (
22 22 dateutil,
23 23 stringutil,
24 24 )
25 25
26 26
27 27 class ResourceUnavailable(error.Abort):
28 28 pass
29 29
30 30
31 31 class TemplateNotFound(error.Abort):
32 32 pass
33 33
34 34
35 35 class wrapped(object): # pytype: disable=ignored-metaclass
36 36 """Object requiring extra conversion prior to displaying or processing
37 37 as value
38 38
39 39 Use unwrapvalue() or unwrapastype() to obtain the inner object.
40 40 """
41 41
42 42 __metaclass__ = abc.ABCMeta
43 43
44 44 @abc.abstractmethod
45 45 def contains(self, context, mapping, item):
46 46 """Test if the specified item is in self
47 47
48 48 The item argument may be a wrapped object.
49 49 """
50 50
51 51 @abc.abstractmethod
52 52 def getmember(self, context, mapping, key):
53 53 """Return a member item for the specified key
54 54
55 55 The key argument may be a wrapped object.
56 56 A returned object may be either a wrapped object or a pure value
57 57 depending on the self type.
58 58 """
59 59
60 60 @abc.abstractmethod
61 61 def getmin(self, context, mapping):
62 62 """Return the smallest item, which may be either a wrapped or a pure
63 63 value depending on the self type"""
64 64
65 65 @abc.abstractmethod
66 66 def getmax(self, context, mapping):
67 67 """Return the largest item, which may be either a wrapped or a pure
68 68 value depending on the self type"""
69 69
70 70 @abc.abstractmethod
71 71 def filter(self, context, mapping, select):
72 72 """Return new container of the same type which includes only the
73 73 selected elements
74 74
75 75 select() takes each item as a wrapped object and returns True/False.
76 76 """
77 77
78 78 @abc.abstractmethod
79 79 def itermaps(self, context):
80 80 """Yield each template mapping"""
81 81
82 82 @abc.abstractmethod
83 83 def join(self, context, mapping, sep):
84 84 """Join items with the separator; Returns a bytes or (possibly nested)
85 85 generator of bytes
86 86
87 87 A pre-configured template may be rendered per item if this container
88 88 holds unprintable items.
89 89 """
90 90
91 91 @abc.abstractmethod
92 92 def show(self, context, mapping):
93 93 """Return a bytes or (possibly nested) generator of bytes representing
94 94 the underlying object
95 95
96 96 A pre-configured template may be rendered if the underlying object is
97 97 not printable.
98 98 """
99 99
100 100 @abc.abstractmethod
101 101 def tobool(self, context, mapping):
102 102 """Return a boolean representation of the inner value"""
103 103
104 104 @abc.abstractmethod
105 105 def tovalue(self, context, mapping):
106 106 """Move the inner value object out or create a value representation
107 107
108 108 A returned value must be serializable by templaterfilters.json().
109 109 """
110 110
111 111
112 112 class mappable(object): # pytype: disable=ignored-metaclass
113 113 """Object which can be converted to a single template mapping"""
114 114
115 115 __metaclass__ = abc.ABCMeta
116 116
117 117 def itermaps(self, context):
118 118 yield self.tomap(context)
119 119
120 120 @abc.abstractmethod
121 121 def tomap(self, context):
122 122 """Create a single template mapping representing this"""
123 123
124 124
125 125 class wrappedbytes(wrapped):
126 126 """Wrapper for byte string"""
127 127
128 128 def __init__(self, value):
129 129 self._value = value
130 130
131 131 def contains(self, context, mapping, item):
132 132 item = stringify(context, mapping, item)
133 133 return item in self._value
134 134
135 135 def getmember(self, context, mapping, key):
136 136 raise error.ParseError(
137 137 _(b'%r is not a dictionary') % pycompat.bytestr(self._value)
138 138 )
139 139
140 140 def getmin(self, context, mapping):
141 141 return self._getby(context, mapping, min)
142 142
143 143 def getmax(self, context, mapping):
144 144 return self._getby(context, mapping, max)
145 145
146 146 def _getby(self, context, mapping, func):
147 147 if not self._value:
148 148 raise error.ParseError(_(b'empty string'))
149 149 return func(pycompat.iterbytestr(self._value))
150 150
151 151 def filter(self, context, mapping, select):
152 152 raise error.ParseError(
153 153 _(b'%r is not filterable') % pycompat.bytestr(self._value)
154 154 )
155 155
156 156 def itermaps(self, context):
157 157 raise error.ParseError(
158 158 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value)
159 159 )
160 160
161 161 def join(self, context, mapping, sep):
162 162 return joinitems(pycompat.iterbytestr(self._value), sep)
163 163
164 164 def show(self, context, mapping):
165 165 return self._value
166 166
167 167 def tobool(self, context, mapping):
168 168 return bool(self._value)
169 169
170 170 def tovalue(self, context, mapping):
171 171 return self._value
172 172
173 173
174 174 class wrappedvalue(wrapped):
175 175 """Generic wrapper for pure non-list/dict/bytes value"""
176 176
177 177 def __init__(self, value):
178 178 self._value = value
179 179
180 180 def contains(self, context, mapping, item):
181 181 raise error.ParseError(_(b"%r is not iterable") % self._value)
182 182
183 183 def getmember(self, context, mapping, key):
184 184 raise error.ParseError(_(b'%r is not a dictionary') % self._value)
185 185
186 186 def getmin(self, context, mapping):
187 187 raise error.ParseError(_(b"%r is not iterable") % self._value)
188 188
189 189 def getmax(self, context, mapping):
190 190 raise error.ParseError(_(b"%r is not iterable") % self._value)
191 191
192 192 def filter(self, context, mapping, select):
193 193 raise error.ParseError(_(b"%r is not iterable") % self._value)
194 194
195 195 def itermaps(self, context):
196 196 raise error.ParseError(
197 197 _(b'%r is not iterable of mappings') % self._value
198 198 )
199 199
200 200 def join(self, context, mapping, sep):
201 201 raise error.ParseError(_(b'%r is not iterable') % self._value)
202 202
203 203 def show(self, context, mapping):
204 204 if self._value is None:
205 205 return b''
206 206 return pycompat.bytestr(self._value)
207 207
208 208 def tobool(self, context, mapping):
209 209 if self._value is None:
210 210 return False
211 211 if isinstance(self._value, bool):
212 212 return self._value
213 213 # otherwise evaluate as string, which means 0 is True
214 214 return bool(pycompat.bytestr(self._value))
215 215
216 216 def tovalue(self, context, mapping):
217 217 return self._value
218 218
219 219
220 220 class date(mappable, wrapped):
221 221 """Wrapper for date tuple"""
222 222
223 223 def __init__(self, value, showfmt=b'%d %d'):
224 224 # value may be (float, int), but public interface shouldn't support
225 225 # floating-point timestamp
226 226 self._unixtime, self._tzoffset = map(int, value)
227 227 self._showfmt = showfmt
228 228
229 229 def contains(self, context, mapping, item):
230 230 raise error.ParseError(_(b'date is not iterable'))
231 231
232 232 def getmember(self, context, mapping, key):
233 233 raise error.ParseError(_(b'date is not a dictionary'))
234 234
235 235 def getmin(self, context, mapping):
236 236 raise error.ParseError(_(b'date is not iterable'))
237 237
238 238 def getmax(self, context, mapping):
239 239 raise error.ParseError(_(b'date is not iterable'))
240 240
241 241 def filter(self, context, mapping, select):
242 242 raise error.ParseError(_(b'date is not iterable'))
243 243
244 244 def join(self, context, mapping, sep):
245 245 raise error.ParseError(_(b"date is not iterable"))
246 246
247 247 def show(self, context, mapping):
248 248 return self._showfmt % (self._unixtime, self._tzoffset)
249 249
250 250 def tomap(self, context):
251 251 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset}
252 252
253 253 def tobool(self, context, mapping):
254 254 return True
255 255
256 256 def tovalue(self, context, mapping):
257 257 return (self._unixtime, self._tzoffset)
258 258
259 259
260 260 class hybrid(wrapped):
261 261 """Wrapper for list or dict to support legacy template
262 262
263 263 This class allows us to handle both:
264 264 - "{files}" (legacy command-line-specific list hack) and
265 265 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
266 266 and to access raw values:
267 267 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
268 268 - "{get(extras, key)}"
269 269 - "{files|json}"
270 270 """
271 271
272 272 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
273 273 self._gen = gen # generator or function returning generator
274 274 self._values = values
275 275 self._makemap = makemap
276 276 self._joinfmt = joinfmt
277 277 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
278 278
279 279 def contains(self, context, mapping, item):
280 280 item = unwrapastype(context, mapping, item, self._keytype)
281 281 return item in self._values
282 282
283 283 def getmember(self, context, mapping, key):
284 284 # TODO: maybe split hybrid list/dict types?
285 285 if not util.safehasattr(self._values, b'get'):
286 286 raise error.ParseError(_(b'not a dictionary'))
287 287 key = unwrapastype(context, mapping, key, self._keytype)
288 288 return self._wrapvalue(key, self._values.get(key))
289 289
290 290 def getmin(self, context, mapping):
291 291 return self._getby(context, mapping, min)
292 292
293 293 def getmax(self, context, mapping):
294 294 return self._getby(context, mapping, max)
295 295
296 296 def _getby(self, context, mapping, func):
297 297 if not self._values:
298 298 raise error.ParseError(_(b'empty sequence'))
299 299 val = func(self._values)
300 300 return self._wrapvalue(val, val)
301 301
302 302 def _wrapvalue(self, key, val):
303 303 if val is None:
304 304 return
305 305 if util.safehasattr(val, b'_makemap'):
306 306 # a nested hybrid list/dict, which has its own way of map operation
307 307 return val
308 308 return hybriditem(None, key, val, self._makemap)
309 309
310 310 def filter(self, context, mapping, select):
311 311 if util.safehasattr(self._values, b'get'):
312 312 values = {
313 313 k: v
314 314 for k, v in pycompat.iteritems(self._values)
315 315 if select(self._wrapvalue(k, v))
316 316 }
317 317 else:
318 318 values = [v for v in self._values if select(self._wrapvalue(v, v))]
319 319 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
320 320
321 321 def itermaps(self, context):
322 322 makemap = self._makemap
323 323 for x in self._values:
324 324 yield makemap(x)
325 325
326 326 def join(self, context, mapping, sep):
327 327 # TODO: switch gen to (context, mapping) API?
328 328 return joinitems((self._joinfmt(x) for x in self._values), sep)
329 329
330 330 def show(self, context, mapping):
331 331 # TODO: switch gen to (context, mapping) API?
332 332 gen = self._gen
333 333 if gen is None:
334 334 return self.join(context, mapping, b' ')
335 335 if callable(gen):
336 336 return gen()
337 337 return gen
338 338
339 339 def tobool(self, context, mapping):
340 340 return bool(self._values)
341 341
342 342 def tovalue(self, context, mapping):
343 343 # TODO: make it non-recursive for trivial lists/dicts
344 344 xs = self._values
345 345 if util.safehasattr(xs, b'get'):
346 346 return {
347 347 k: unwrapvalue(context, mapping, v)
348 348 for k, v in pycompat.iteritems(xs)
349 349 }
350 350 return [unwrapvalue(context, mapping, x) for x in xs]
351 351
352 352
353 353 class hybriditem(mappable, wrapped):
354 354 """Wrapper for non-list/dict object to support map operation
355 355
356 356 This class allows us to handle both:
357 357 - "{manifest}"
358 358 - "{manifest % '{rev}:{node}'}"
359 359 - "{manifest.rev}"
360 360 """
361 361
362 362 def __init__(self, gen, key, value, makemap):
363 363 self._gen = gen # generator or function returning generator
364 364 self._key = key
365 365 self._value = value # may be generator of strings
366 366 self._makemap = makemap
367 367
368 368 def tomap(self, context):
369 369 return self._makemap(self._key)
370 370
371 371 def contains(self, context, mapping, item):
372 372 w = makewrapped(context, mapping, self._value)
373 373 return w.contains(context, mapping, item)
374 374
375 375 def getmember(self, context, mapping, key):
376 376 w = makewrapped(context, mapping, self._value)
377 377 return w.getmember(context, mapping, key)
378 378
379 379 def getmin(self, context, mapping):
380 380 w = makewrapped(context, mapping, self._value)
381 381 return w.getmin(context, mapping)
382 382
383 383 def getmax(self, context, mapping):
384 384 w = makewrapped(context, mapping, self._value)
385 385 return w.getmax(context, mapping)
386 386
387 387 def filter(self, context, mapping, select):
388 388 w = makewrapped(context, mapping, self._value)
389 389 return w.filter(context, mapping, select)
390 390
391 391 def join(self, context, mapping, sep):
392 392 w = makewrapped(context, mapping, self._value)
393 393 return w.join(context, mapping, sep)
394 394
395 395 def show(self, context, mapping):
396 396 # TODO: switch gen to (context, mapping) API?
397 397 gen = self._gen
398 398 if gen is None:
399 399 return pycompat.bytestr(self._value)
400 400 if callable(gen):
401 401 return gen()
402 402 return gen
403 403
404 404 def tobool(self, context, mapping):
405 405 w = makewrapped(context, mapping, self._value)
406 406 return w.tobool(context, mapping)
407 407
408 408 def tovalue(self, context, mapping):
409 409 return _unthunk(context, mapping, self._value)
410 410
411 411
412 412 class revslist(wrapped):
413 413 """Wrapper for a smartset (a list/set of revision numbers)
414 414
415 415 If name specified, the revs will be rendered with the old-style list
416 416 template of the given name by default.
417
418 The cachekey provides a hint to cache further computation on this
419 smartset. If the underlying smartset is dynamically created, the cachekey
420 should be None.
417 421 """
418 422
419 def __init__(self, repo, revs, name=None):
423 def __init__(self, repo, revs, name=None, cachekey=None):
420 424 assert isinstance(revs, smartset.abstractsmartset)
421 425 self._repo = repo
422 426 self._revs = revs
423 427 self._name = name
428 self.cachekey = cachekey
424 429
425 430 def contains(self, context, mapping, item):
426 431 rev = unwrapinteger(context, mapping, item)
427 432 return rev in self._revs
428 433
429 434 def getmember(self, context, mapping, key):
430 435 raise error.ParseError(_(b'not a dictionary'))
431 436
432 437 def getmin(self, context, mapping):
433 438 makehybriditem = self._makehybriditemfunc()
434 439 return makehybriditem(self._revs.min())
435 440
436 441 def getmax(self, context, mapping):
437 442 makehybriditem = self._makehybriditemfunc()
438 443 return makehybriditem(self._revs.max())
439 444
440 445 def filter(self, context, mapping, select):
441 446 makehybriditem = self._makehybriditemfunc()
442 447 frevs = self._revs.filter(lambda r: select(makehybriditem(r)))
443 448 # once filtered, no need to support old-style list template
444 449 return revslist(self._repo, frevs, name=None)
445 450
446 451 def itermaps(self, context):
447 452 makemap = self._makemapfunc()
448 453 for r in self._revs:
449 454 yield makemap(r)
450 455
451 456 def _makehybriditemfunc(self):
452 457 makemap = self._makemapfunc()
453 458 return lambda r: hybriditem(None, r, r, makemap)
454 459
455 460 def _makemapfunc(self):
456 461 repo = self._repo
457 462 name = self._name
458 463 if name:
459 464 return lambda r: {name: r, b'ctx': repo[r]}
460 465 else:
461 466 return lambda r: {b'ctx': repo[r]}
462 467
463 468 def join(self, context, mapping, sep):
464 469 return joinitems(self._revs, sep)
465 470
466 471 def show(self, context, mapping):
467 472 if self._name:
468 473 srevs = [b'%d' % r for r in self._revs]
469 474 return _showcompatlist(context, mapping, self._name, srevs)
470 475 else:
471 476 return self.join(context, mapping, b' ')
472 477
473 478 def tobool(self, context, mapping):
474 479 return bool(self._revs)
475 480
476 481 def tovalue(self, context, mapping):
477 482 return self._revs
478 483
479 484
480 485 class _mappingsequence(wrapped):
481 486 """Wrapper for sequence of template mappings
482 487
483 488 This represents an inner template structure (i.e. a list of dicts),
484 489 which can also be rendered by the specified named/literal template.
485 490
486 491 Template mappings may be nested.
487 492 """
488 493
489 494 def __init__(self, name=None, tmpl=None, sep=b''):
490 495 if name is not None and tmpl is not None:
491 496 raise error.ProgrammingError(
492 497 b'name and tmpl are mutually exclusive'
493 498 )
494 499 self._name = name
495 500 self._tmpl = tmpl
496 501 self._defaultsep = sep
497 502
498 503 def contains(self, context, mapping, item):
499 504 raise error.ParseError(_(b'not comparable'))
500 505
501 506 def getmember(self, context, mapping, key):
502 507 raise error.ParseError(_(b'not a dictionary'))
503 508
504 509 def getmin(self, context, mapping):
505 510 raise error.ParseError(_(b'not comparable'))
506 511
507 512 def getmax(self, context, mapping):
508 513 raise error.ParseError(_(b'not comparable'))
509 514
510 515 def filter(self, context, mapping, select):
511 516 # implement if necessary; we'll need a wrapped type for a mapping dict
512 517 raise error.ParseError(_(b'not filterable without template'))
513 518
514 519 def join(self, context, mapping, sep):
515 520 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
516 521 if self._name:
517 522 itemiter = (context.process(self._name, m) for m in mapsiter)
518 523 elif self._tmpl:
519 524 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
520 525 else:
521 526 raise error.ParseError(_(b'not displayable without template'))
522 527 return joinitems(itemiter, sep)
523 528
524 529 def show(self, context, mapping):
525 530 return self.join(context, mapping, self._defaultsep)
526 531
527 532 def tovalue(self, context, mapping):
528 533 knownres = context.knownresourcekeys()
529 534 items = []
530 535 for nm in self.itermaps(context):
531 536 # drop internal resources (recursively) which shouldn't be displayed
532 537 lm = context.overlaymap(mapping, nm)
533 538 items.append(
534 539 {
535 540 k: unwrapvalue(context, lm, v)
536 541 for k, v in pycompat.iteritems(nm)
537 542 if k not in knownres
538 543 }
539 544 )
540 545 return items
541 546
542 547
543 548 class mappinggenerator(_mappingsequence):
544 549 """Wrapper for generator of template mappings
545 550
546 551 The function ``make(context, *args)`` should return a generator of
547 552 mapping dicts.
548 553 """
549 554
550 555 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
551 556 super(mappinggenerator, self).__init__(name, tmpl, sep)
552 557 self._make = make
553 558 self._args = args
554 559
555 560 def itermaps(self, context):
556 561 return self._make(context, *self._args)
557 562
558 563 def tobool(self, context, mapping):
559 564 return _nonempty(self.itermaps(context))
560 565
561 566
562 567 class mappinglist(_mappingsequence):
563 568 """Wrapper for list of template mappings"""
564 569
565 570 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
566 571 super(mappinglist, self).__init__(name, tmpl, sep)
567 572 self._mappings = mappings
568 573
569 574 def itermaps(self, context):
570 575 return iter(self._mappings)
571 576
572 577 def tobool(self, context, mapping):
573 578 return bool(self._mappings)
574 579
575 580
576 581 class mappingdict(mappable, _mappingsequence):
577 582 """Wrapper for a single template mapping
578 583
579 584 This isn't a sequence in a way that the underlying dict won't be iterated
580 585 as a dict, but shares most of the _mappingsequence functions.
581 586 """
582 587
583 588 def __init__(self, mapping, name=None, tmpl=None):
584 589 super(mappingdict, self).__init__(name, tmpl)
585 590 self._mapping = mapping
586 591
587 592 def tomap(self, context):
588 593 return self._mapping
589 594
590 595 def tobool(self, context, mapping):
591 596 # no idea when a template mapping should be considered an empty, but
592 597 # a mapping dict should have at least one item in practice, so always
593 598 # mark this as non-empty.
594 599 return True
595 600
596 601 def tovalue(self, context, mapping):
597 602 return super(mappingdict, self).tovalue(context, mapping)[0]
598 603
599 604
600 605 class mappingnone(wrappedvalue):
601 606 """Wrapper for None, but supports map operation
602 607
603 608 This represents None of Optional[mappable]. It's similar to
604 609 mapplinglist([]), but the underlying value is not [], but None.
605 610 """
606 611
607 612 def __init__(self):
608 613 super(mappingnone, self).__init__(None)
609 614
610 615 def itermaps(self, context):
611 616 return iter([])
612 617
613 618
614 619 class mappedgenerator(wrapped):
615 620 """Wrapper for generator of strings which acts as a list
616 621
617 622 The function ``make(context, *args)`` should return a generator of
618 623 byte strings, or a generator of (possibly nested) generators of byte
619 624 strings (i.e. a generator for a list of byte strings.)
620 625 """
621 626
622 627 def __init__(self, make, args=()):
623 628 self._make = make
624 629 self._args = args
625 630
626 631 def contains(self, context, mapping, item):
627 632 item = stringify(context, mapping, item)
628 633 return item in self.tovalue(context, mapping)
629 634
630 635 def _gen(self, context):
631 636 return self._make(context, *self._args)
632 637
633 638 def getmember(self, context, mapping, key):
634 639 raise error.ParseError(_(b'not a dictionary'))
635 640
636 641 def getmin(self, context, mapping):
637 642 return self._getby(context, mapping, min)
638 643
639 644 def getmax(self, context, mapping):
640 645 return self._getby(context, mapping, max)
641 646
642 647 def _getby(self, context, mapping, func):
643 648 xs = self.tovalue(context, mapping)
644 649 if not xs:
645 650 raise error.ParseError(_(b'empty sequence'))
646 651 return func(xs)
647 652
648 653 @staticmethod
649 654 def _filteredgen(context, mapping, make, args, select):
650 655 for x in make(context, *args):
651 656 s = stringify(context, mapping, x)
652 657 if select(wrappedbytes(s)):
653 658 yield s
654 659
655 660 def filter(self, context, mapping, select):
656 661 args = (mapping, self._make, self._args, select)
657 662 return mappedgenerator(self._filteredgen, args)
658 663
659 664 def itermaps(self, context):
660 665 raise error.ParseError(_(b'list of strings is not mappable'))
661 666
662 667 def join(self, context, mapping, sep):
663 668 return joinitems(self._gen(context), sep)
664 669
665 670 def show(self, context, mapping):
666 671 return self.join(context, mapping, b'')
667 672
668 673 def tobool(self, context, mapping):
669 674 return _nonempty(self._gen(context))
670 675
671 676 def tovalue(self, context, mapping):
672 677 return [stringify(context, mapping, x) for x in self._gen(context)]
673 678
674 679
675 680 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
676 681 """Wrap data to support both dict-like and string-like operations"""
677 682 prefmt = pycompat.identity
678 683 if fmt is None:
679 684 fmt = b'%s=%s'
680 685 prefmt = pycompat.bytestr
681 686 return hybrid(
682 687 gen,
683 688 data,
684 689 lambda k: {key: k, value: data[k]},
685 690 lambda k: fmt % (prefmt(k), prefmt(data[k])),
686 691 )
687 692
688 693
689 694 def hybridlist(data, name, fmt=None, gen=None):
690 695 """Wrap data to support both list-like and string-like operations"""
691 696 prefmt = pycompat.identity
692 697 if fmt is None:
693 698 fmt = b'%s'
694 699 prefmt = pycompat.bytestr
695 700 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
696 701
697 702
698 703 def compatdict(
699 704 context,
700 705 mapping,
701 706 name,
702 707 data,
703 708 key=b'key',
704 709 value=b'value',
705 710 fmt=None,
706 711 plural=None,
707 712 separator=b' ',
708 713 ):
709 714 """Wrap data like hybriddict(), but also supports old-style list template
710 715
711 716 This exists for backward compatibility with the old-style template. Use
712 717 hybriddict() for new template keywords.
713 718 """
714 719 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
715 720 f = _showcompatlist(context, mapping, name, c, plural, separator)
716 721 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
717 722
718 723
719 724 def compatlist(
720 725 context,
721 726 mapping,
722 727 name,
723 728 data,
724 729 element=None,
725 730 fmt=None,
726 731 plural=None,
727 732 separator=b' ',
728 733 ):
729 734 """Wrap data like hybridlist(), but also supports old-style list template
730 735
731 736 This exists for backward compatibility with the old-style template. Use
732 737 hybridlist() for new template keywords.
733 738 """
734 739 f = _showcompatlist(context, mapping, name, data, plural, separator)
735 740 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
736 741
737 742
738 743 def compatfilecopiesdict(context, mapping, name, copies):
739 744 """Wrap list of (dest, source) file names to support old-style list
740 745 template and field names
741 746
742 747 This exists for backward compatibility. Use hybriddict for new template
743 748 keywords.
744 749 """
745 750 # no need to provide {path} to old-style list template
746 751 c = [{b'name': k, b'source': v} for k, v in copies]
747 752 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
748 753 copies = util.sortdict(copies)
749 754 return hybrid(
750 755 f,
751 756 copies,
752 757 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
753 758 lambda k: b'%s (%s)' % (k, copies[k]),
754 759 )
755 760
756 761
757 762 def compatfileslist(context, mapping, name, files):
758 763 """Wrap list of file names to support old-style list template and field
759 764 names
760 765
761 766 This exists for backward compatibility. Use hybridlist for new template
762 767 keywords.
763 768 """
764 769 f = _showcompatlist(context, mapping, name, files)
765 770 return hybrid(
766 771 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
767 772 )
768 773
769 774
770 775 def _showcompatlist(
771 776 context, mapping, name, values, plural=None, separator=b' '
772 777 ):
773 778 """Return a generator that renders old-style list template
774 779
775 780 name is name of key in template map.
776 781 values is list of strings or dicts.
777 782 plural is plural of name, if not simply name + 's'.
778 783 separator is used to join values as a string
779 784
780 785 expansion works like this, given name 'foo'.
781 786
782 787 if values is empty, expand 'no_foos'.
783 788
784 789 if 'foo' not in template map, return values as a string,
785 790 joined by 'separator'.
786 791
787 792 expand 'start_foos'.
788 793
789 794 for each value, expand 'foo'. if 'last_foo' in template
790 795 map, expand it instead of 'foo' for last key.
791 796
792 797 expand 'end_foos'.
793 798 """
794 799 if not plural:
795 800 plural = name + b's'
796 801 if not values:
797 802 noname = b'no_' + plural
798 803 if context.preload(noname):
799 804 yield context.process(noname, mapping)
800 805 return
801 806 if not context.preload(name):
802 807 if isinstance(values[0], bytes):
803 808 yield separator.join(values)
804 809 else:
805 810 for v in values:
806 811 r = dict(v)
807 812 r.update(mapping)
808 813 yield r
809 814 return
810 815 startname = b'start_' + plural
811 816 if context.preload(startname):
812 817 yield context.process(startname, mapping)
813 818
814 819 def one(v, tag=name):
815 820 vmapping = {}
816 821 try:
817 822 vmapping.update(v)
818 823 # Python 2 raises ValueError if the type of v is wrong. Python
819 824 # 3 raises TypeError.
820 825 except (AttributeError, TypeError, ValueError):
821 826 try:
822 827 # Python 2 raises ValueError trying to destructure an e.g.
823 828 # bytes. Python 3 raises TypeError.
824 829 for a, b in v:
825 830 vmapping[a] = b
826 831 except (TypeError, ValueError):
827 832 vmapping[name] = v
828 833 vmapping = context.overlaymap(mapping, vmapping)
829 834 return context.process(tag, vmapping)
830 835
831 836 lastname = b'last_' + name
832 837 if context.preload(lastname):
833 838 last = values.pop()
834 839 else:
835 840 last = None
836 841 for v in values:
837 842 yield one(v)
838 843 if last is not None:
839 844 yield one(last, tag=lastname)
840 845 endname = b'end_' + plural
841 846 if context.preload(endname):
842 847 yield context.process(endname, mapping)
843 848
844 849
845 850 def flatten(context, mapping, thing):
846 851 """Yield a single stream from a possibly nested set of iterators"""
847 852 if isinstance(thing, wrapped):
848 853 thing = thing.show(context, mapping)
849 854 if isinstance(thing, bytes):
850 855 yield thing
851 856 elif isinstance(thing, str):
852 857 # We can only hit this on Python 3, and it's here to guard
853 858 # against infinite recursion.
854 859 raise error.ProgrammingError(
855 860 b'Mercurial IO including templates is done'
856 861 b' with bytes, not strings, got %r' % thing
857 862 )
858 863 elif thing is None:
859 864 pass
860 865 elif not util.safehasattr(thing, b'__iter__'):
861 866 yield pycompat.bytestr(thing)
862 867 else:
863 868 for i in thing:
864 869 if isinstance(i, wrapped):
865 870 i = i.show(context, mapping)
866 871 if isinstance(i, bytes):
867 872 yield i
868 873 elif i is None:
869 874 pass
870 875 elif not util.safehasattr(i, b'__iter__'):
871 876 yield pycompat.bytestr(i)
872 877 else:
873 878 for j in flatten(context, mapping, i):
874 879 yield j
875 880
876 881
877 882 def stringify(context, mapping, thing):
878 883 """Turn values into bytes by converting into text and concatenating them"""
879 884 if isinstance(thing, bytes):
880 885 return thing # retain localstr to be round-tripped
881 886 return b''.join(flatten(context, mapping, thing))
882 887
883 888
884 889 def findsymbolicname(arg):
885 890 """Find symbolic name for the given compiled expression; returns None
886 891 if nothing found reliably"""
887 892 while True:
888 893 func, data = arg
889 894 if func is runsymbol:
890 895 return data
891 896 elif func is runfilter:
892 897 arg = data[0]
893 898 else:
894 899 return None
895 900
896 901
897 902 def _nonempty(xiter):
898 903 try:
899 904 next(xiter)
900 905 return True
901 906 except StopIteration:
902 907 return False
903 908
904 909
905 910 def _unthunk(context, mapping, thing):
906 911 """Evaluate a lazy byte string into value"""
907 912 if not isinstance(thing, types.GeneratorType):
908 913 return thing
909 914 return stringify(context, mapping, thing)
910 915
911 916
912 917 def evalrawexp(context, mapping, arg):
913 918 """Evaluate given argument as a bare template object which may require
914 919 further processing (such as folding generator of strings)"""
915 920 func, data = arg
916 921 return func(context, mapping, data)
917 922
918 923
919 924 def evalwrapped(context, mapping, arg):
920 925 """Evaluate given argument to wrapped object"""
921 926 thing = evalrawexp(context, mapping, arg)
922 927 return makewrapped(context, mapping, thing)
923 928
924 929
925 930 def makewrapped(context, mapping, thing):
926 931 """Lift object to a wrapped type"""
927 932 if isinstance(thing, wrapped):
928 933 return thing
929 934 thing = _unthunk(context, mapping, thing)
930 935 if isinstance(thing, bytes):
931 936 return wrappedbytes(thing)
932 937 return wrappedvalue(thing)
933 938
934 939
935 940 def evalfuncarg(context, mapping, arg):
936 941 """Evaluate given argument as value type"""
937 942 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
938 943
939 944
940 945 def unwrapvalue(context, mapping, thing):
941 946 """Move the inner value object out of the wrapper"""
942 947 if isinstance(thing, wrapped):
943 948 return thing.tovalue(context, mapping)
944 949 # evalrawexp() may return string, generator of strings or arbitrary object
945 950 # such as date tuple, but filter does not want generator.
946 951 return _unthunk(context, mapping, thing)
947 952
948 953
949 954 def evalboolean(context, mapping, arg):
950 955 """Evaluate given argument as boolean, but also takes boolean literals"""
951 956 func, data = arg
952 957 if func is runsymbol:
953 958 thing = func(context, mapping, data, default=None)
954 959 if thing is None:
955 960 # not a template keyword, takes as a boolean literal
956 961 thing = stringutil.parsebool(data)
957 962 else:
958 963 thing = func(context, mapping, data)
959 964 return makewrapped(context, mapping, thing).tobool(context, mapping)
960 965
961 966
962 967 def evaldate(context, mapping, arg, err=None):
963 968 """Evaluate given argument as a date tuple or a date string; returns
964 969 a (unixtime, offset) tuple"""
965 970 thing = evalrawexp(context, mapping, arg)
966 971 return unwrapdate(context, mapping, thing, err)
967 972
968 973
969 974 def unwrapdate(context, mapping, thing, err=None):
970 975 if isinstance(thing, date):
971 976 return thing.tovalue(context, mapping)
972 977 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
973 978 thing = unwrapvalue(context, mapping, thing)
974 979 try:
975 980 return dateutil.parsedate(thing)
976 981 except AttributeError:
977 982 raise error.ParseError(err or _(b'not a date tuple nor a string'))
978 983 except error.ParseError:
979 984 if not err:
980 985 raise
981 986 raise error.ParseError(err)
982 987
983 988
984 989 def evalinteger(context, mapping, arg, err=None):
985 990 thing = evalrawexp(context, mapping, arg)
986 991 return unwrapinteger(context, mapping, thing, err)
987 992
988 993
989 994 def unwrapinteger(context, mapping, thing, err=None):
990 995 thing = unwrapvalue(context, mapping, thing)
991 996 try:
992 997 return int(thing)
993 998 except (TypeError, ValueError):
994 999 raise error.ParseError(err or _(b'not an integer'))
995 1000
996 1001
997 1002 def evalstring(context, mapping, arg):
998 1003 return stringify(context, mapping, evalrawexp(context, mapping, arg))
999 1004
1000 1005
1001 1006 def evalstringliteral(context, mapping, arg):
1002 1007 """Evaluate given argument as string template, but returns symbol name
1003 1008 if it is unknown"""
1004 1009 func, data = arg
1005 1010 if func is runsymbol:
1006 1011 thing = func(context, mapping, data, default=data)
1007 1012 else:
1008 1013 thing = func(context, mapping, data)
1009 1014 return stringify(context, mapping, thing)
1010 1015
1011 1016
1012 1017 _unwrapfuncbytype = {
1013 1018 None: unwrapvalue,
1014 1019 bytes: stringify,
1015 1020 date: unwrapdate,
1016 1021 int: unwrapinteger,
1017 1022 }
1018 1023
1019 1024
1020 1025 def unwrapastype(context, mapping, thing, typ):
1021 1026 """Move the inner value object out of the wrapper and coerce its type"""
1022 1027 try:
1023 1028 f = _unwrapfuncbytype[typ]
1024 1029 except KeyError:
1025 1030 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
1026 1031 return f(context, mapping, thing)
1027 1032
1028 1033
1029 1034 def runinteger(context, mapping, data):
1030 1035 return int(data)
1031 1036
1032 1037
1033 1038 def runstring(context, mapping, data):
1034 1039 return data
1035 1040
1036 1041
1037 1042 def _recursivesymbolblocker(key):
1038 1043 def showrecursion(context, mapping):
1039 1044 raise error.Abort(_(b"recursive reference '%s' in template") % key)
1040 1045
1041 1046 return showrecursion
1042 1047
1043 1048
1044 1049 def runsymbol(context, mapping, key, default=b''):
1045 1050 v = context.symbol(mapping, key)
1046 1051 if v is None:
1047 1052 # put poison to cut recursion. we can't move this to parsing phase
1048 1053 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
1049 1054 safemapping = mapping.copy()
1050 1055 safemapping[key] = _recursivesymbolblocker(key)
1051 1056 try:
1052 1057 v = context.process(key, safemapping)
1053 1058 except TemplateNotFound:
1054 1059 v = default
1055 1060 if callable(v):
1056 1061 # new templatekw
1057 1062 try:
1058 1063 return v(context, mapping)
1059 1064 except ResourceUnavailable:
1060 1065 # unsupported keyword is mapped to empty just like unknown keyword
1061 1066 return None
1062 1067 return v
1063 1068
1064 1069
1065 1070 def runtemplate(context, mapping, template):
1066 1071 for arg in template:
1067 1072 yield evalrawexp(context, mapping, arg)
1068 1073
1069 1074
1070 1075 def runfilter(context, mapping, data):
1071 1076 arg, filt = data
1072 1077 thing = evalrawexp(context, mapping, arg)
1073 1078 intype = getattr(filt, '_intype', None)
1074 1079 try:
1075 1080 thing = unwrapastype(context, mapping, thing, intype)
1076 1081 return filt(thing)
1077 1082 except error.ParseError as e:
1078 1083 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1079 1084
1080 1085
1081 1086 def _formatfiltererror(arg, filt):
1082 1087 fn = pycompat.sysbytes(filt.__name__)
1083 1088 sym = findsymbolicname(arg)
1084 1089 if not sym:
1085 1090 return _(b"incompatible use of template filter '%s'") % fn
1086 1091 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1087 1092 fn,
1088 1093 sym,
1089 1094 )
1090 1095
1091 1096
1092 1097 def _iteroverlaymaps(context, origmapping, newmappings):
1093 1098 """Generate combined mappings from the original mapping and an iterable
1094 1099 of partial mappings to override the original"""
1095 1100 for i, nm in enumerate(newmappings):
1096 1101 lm = context.overlaymap(origmapping, nm)
1097 1102 lm[b'index'] = i
1098 1103 yield lm
1099 1104
1100 1105
1101 1106 def _applymap(context, mapping, d, darg, targ):
1102 1107 try:
1103 1108 diter = d.itermaps(context)
1104 1109 except error.ParseError as err:
1105 1110 sym = findsymbolicname(darg)
1106 1111 if not sym:
1107 1112 raise
1108 1113 hint = _(b"keyword '%s' does not support map operation") % sym
1109 1114 raise error.ParseError(bytes(err), hint=hint)
1110 1115 for lm in _iteroverlaymaps(context, mapping, diter):
1111 1116 yield evalrawexp(context, lm, targ)
1112 1117
1113 1118
1114 1119 def runmap(context, mapping, data):
1115 1120 darg, targ = data
1116 1121 d = evalwrapped(context, mapping, darg)
1117 1122 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1118 1123
1119 1124
1120 1125 def runmember(context, mapping, data):
1121 1126 darg, memb = data
1122 1127 d = evalwrapped(context, mapping, darg)
1123 1128 if isinstance(d, mappable):
1124 1129 lm = context.overlaymap(mapping, d.tomap(context))
1125 1130 return runsymbol(context, lm, memb)
1126 1131 try:
1127 1132 return d.getmember(context, mapping, memb)
1128 1133 except error.ParseError as err:
1129 1134 sym = findsymbolicname(darg)
1130 1135 if not sym:
1131 1136 raise
1132 1137 hint = _(b"keyword '%s' does not support member operation") % sym
1133 1138 raise error.ParseError(bytes(err), hint=hint)
1134 1139
1135 1140
1136 1141 def runnegate(context, mapping, data):
1137 1142 data = evalinteger(
1138 1143 context, mapping, data, _(b'negation needs an integer argument')
1139 1144 )
1140 1145 return -data
1141 1146
1142 1147
1143 1148 def runarithmetic(context, mapping, data):
1144 1149 func, left, right = data
1145 1150 left = evalinteger(
1146 1151 context, mapping, left, _(b'arithmetic only defined on integers')
1147 1152 )
1148 1153 right = evalinteger(
1149 1154 context, mapping, right, _(b'arithmetic only defined on integers')
1150 1155 )
1151 1156 try:
1152 1157 return func(left, right)
1153 1158 except ZeroDivisionError:
1154 1159 raise error.Abort(_(b'division by zero is not defined'))
1155 1160
1156 1161
1157 1162 def joinitems(itemiter, sep):
1158 1163 """Join items with the separator; Returns generator of bytes"""
1159 1164 first = True
1160 1165 for x in itemiter:
1161 1166 if first:
1162 1167 first = False
1163 1168 elif sep:
1164 1169 yield sep
1165 1170 yield x
General Comments 0
You need to be logged in to leave comments. Login now