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