##// END OF EJS Templates
templater: fix cbor() filter to accept smartset...
Yuya Nishihara -
r45466:e3e44e6e default
parent child Browse files
Show More
@@ -1,549 +1,553 b''
1 1 # templatefilters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 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 os
11 11 import re
12 12 import time
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 node,
19 19 pycompat,
20 20 registrar,
21 smartset,
21 22 templateutil,
22 23 url,
23 24 util,
24 25 )
25 26 from .utils import (
26 27 cborutil,
27 28 dateutil,
28 29 stringutil,
29 30 )
30 31
31 32 urlerr = util.urlerr
32 33 urlreq = util.urlreq
33 34
34 35 # filters are callables like:
35 36 # fn(obj)
36 37 # with:
37 38 # obj - object to be filtered (text, date, list and so on)
38 39 filters = {}
39 40
40 41 templatefilter = registrar.templatefilter(filters)
41 42
42 43
43 44 @templatefilter(b'addbreaks', intype=bytes)
44 45 def addbreaks(text):
45 46 """Any text. Add an XHTML "<br />" tag before the end of
46 47 every line except the last.
47 48 """
48 49 return text.replace(b'\n', b'<br/>\n')
49 50
50 51
51 52 agescales = [
52 53 (b"year", 3600 * 24 * 365, b'Y'),
53 54 (b"month", 3600 * 24 * 30, b'M'),
54 55 (b"week", 3600 * 24 * 7, b'W'),
55 56 (b"day", 3600 * 24, b'd'),
56 57 (b"hour", 3600, b'h'),
57 58 (b"minute", 60, b'm'),
58 59 (b"second", 1, b's'),
59 60 ]
60 61
61 62
62 63 @templatefilter(b'age', intype=templateutil.date)
63 64 def age(date, abbrev=False):
64 65 """Date. Returns a human-readable date/time difference between the
65 66 given date/time and the current date/time.
66 67 """
67 68
68 69 def plural(t, c):
69 70 if c == 1:
70 71 return t
71 72 return t + b"s"
72 73
73 74 def fmt(t, c, a):
74 75 if abbrev:
75 76 return b"%d%s" % (c, a)
76 77 return b"%d %s" % (c, plural(t, c))
77 78
78 79 now = time.time()
79 80 then = date[0]
80 81 future = False
81 82 if then > now:
82 83 future = True
83 84 delta = max(1, int(then - now))
84 85 if delta > agescales[0][1] * 30:
85 86 return b'in the distant future'
86 87 else:
87 88 delta = max(1, int(now - then))
88 89 if delta > agescales[0][1] * 2:
89 90 return dateutil.shortdate(date)
90 91
91 92 for t, s, a in agescales:
92 93 n = delta // s
93 94 if n >= 2 or s == 1:
94 95 if future:
95 96 return b'%s from now' % fmt(t, n, a)
96 97 return b'%s ago' % fmt(t, n, a)
97 98
98 99
99 100 @templatefilter(b'basename', intype=bytes)
100 101 def basename(path):
101 102 """Any text. Treats the text as a path, and returns the last
102 103 component of the path after splitting by the path separator.
103 104 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
104 105 """
105 106 return os.path.basename(path)
106 107
107 108
108 109 @templatefilter(b'cbor')
109 110 def cbor(obj):
110 111 """Any object. Serializes the object to CBOR bytes."""
112 if isinstance(obj, smartset.abstractsmartset):
113 # cborutil is stricter about type than json() filter
114 obj = list(obj)
111 115 return b''.join(cborutil.streamencode(obj))
112 116
113 117
114 118 @templatefilter(b'commondir')
115 119 def commondir(filelist):
116 120 """List of text. Treats each list item as file name with /
117 121 as path separator and returns the longest common directory
118 122 prefix shared by all list items.
119 123 Returns the empty string if no common prefix exists.
120 124
121 125 The list items are not normalized, i.e. "foo/../bar" is handled as
122 126 file "bar" in the directory "foo/..". Leading slashes are ignored.
123 127
124 128 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
125 129 ["foo/bar", "baz"] becomes "".
126 130 """
127 131
128 132 def common(a, b):
129 133 if len(a) > len(b):
130 134 a = b[: len(a)]
131 135 elif len(b) > len(a):
132 136 b = b[: len(a)]
133 137 if a == b:
134 138 return a
135 139 for i in pycompat.xrange(len(a)):
136 140 if a[i] != b[i]:
137 141 return a[:i]
138 142 return a
139 143
140 144 try:
141 145 if not filelist:
142 146 return b""
143 147 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
144 148 if len(dirlist) == 1:
145 149 return b'/'.join(dirlist[0])
146 150 a = min(dirlist)
147 151 b = max(dirlist)
148 152 # The common prefix of a and b is shared with all
149 153 # elements of the list since Python sorts lexicographical
150 154 # and [1, x] after [1].
151 155 return b'/'.join(common(a, b))
152 156 except TypeError:
153 157 raise error.ParseError(_(b'argument is not a list of text'))
154 158
155 159
156 160 @templatefilter(b'count')
157 161 def count(i):
158 162 """List or text. Returns the length as an integer."""
159 163 try:
160 164 return len(i)
161 165 except TypeError:
162 166 raise error.ParseError(_(b'not countable'))
163 167
164 168
165 169 @templatefilter(b'dirname', intype=bytes)
166 170 def dirname(path):
167 171 """Any text. Treats the text as a path, and strips the last
168 172 component of the path after splitting by the path separator.
169 173 """
170 174 return os.path.dirname(path)
171 175
172 176
173 177 @templatefilter(b'domain', intype=bytes)
174 178 def domain(author):
175 179 """Any text. Finds the first string that looks like an email
176 180 address, and extracts just the domain component. Example: ``User
177 181 <user@example.com>`` becomes ``example.com``.
178 182 """
179 183 f = author.find(b'@')
180 184 if f == -1:
181 185 return b''
182 186 author = author[f + 1 :]
183 187 f = author.find(b'>')
184 188 if f >= 0:
185 189 author = author[:f]
186 190 return author
187 191
188 192
189 193 @templatefilter(b'email', intype=bytes)
190 194 def email(text):
191 195 """Any text. Extracts the first string that looks like an email
192 196 address. Example: ``User <user@example.com>`` becomes
193 197 ``user@example.com``.
194 198 """
195 199 return stringutil.email(text)
196 200
197 201
198 202 @templatefilter(b'escape', intype=bytes)
199 203 def escape(text):
200 204 """Any text. Replaces the special XML/XHTML characters "&", "<"
201 205 and ">" with XML entities, and filters out NUL characters.
202 206 """
203 207 return url.escape(text.replace(b'\0', b''), True)
204 208
205 209
206 210 para_re = None
207 211 space_re = None
208 212
209 213
210 214 def fill(text, width, initindent=b'', hangindent=b''):
211 215 '''fill many paragraphs with optional indentation.'''
212 216 global para_re, space_re
213 217 if para_re is None:
214 218 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
215 219 space_re = re.compile(br' +')
216 220
217 221 def findparas():
218 222 start = 0
219 223 while True:
220 224 m = para_re.search(text, start)
221 225 if not m:
222 226 uctext = encoding.unifromlocal(text[start:])
223 227 w = len(uctext)
224 228 while w > 0 and uctext[w - 1].isspace():
225 229 w -= 1
226 230 yield (
227 231 encoding.unitolocal(uctext[:w]),
228 232 encoding.unitolocal(uctext[w:]),
229 233 )
230 234 break
231 235 yield text[start : m.start(0)], m.group(1)
232 236 start = m.end(1)
233 237
234 238 return b"".join(
235 239 [
236 240 stringutil.wrap(
237 241 space_re.sub(b' ', stringutil.wrap(para, width)),
238 242 width,
239 243 initindent,
240 244 hangindent,
241 245 )
242 246 + rest
243 247 for para, rest in findparas()
244 248 ]
245 249 )
246 250
247 251
248 252 @templatefilter(b'fill68', intype=bytes)
249 253 def fill68(text):
250 254 """Any text. Wraps the text to fit in 68 columns."""
251 255 return fill(text, 68)
252 256
253 257
254 258 @templatefilter(b'fill76', intype=bytes)
255 259 def fill76(text):
256 260 """Any text. Wraps the text to fit in 76 columns."""
257 261 return fill(text, 76)
258 262
259 263
260 264 @templatefilter(b'firstline', intype=bytes)
261 265 def firstline(text):
262 266 """Any text. Returns the first line of text."""
263 267 try:
264 268 return text.splitlines(True)[0].rstrip(b'\r\n')
265 269 except IndexError:
266 270 return b''
267 271
268 272
269 273 @templatefilter(b'hex', intype=bytes)
270 274 def hexfilter(text):
271 275 """Any text. Convert a binary Mercurial node identifier into
272 276 its long hexadecimal representation.
273 277 """
274 278 return node.hex(text)
275 279
276 280
277 281 @templatefilter(b'hgdate', intype=templateutil.date)
278 282 def hgdate(text):
279 283 """Date. Returns the date as a pair of numbers: "1157407993
280 284 25200" (Unix timestamp, timezone offset).
281 285 """
282 286 return b"%d %d" % text
283 287
284 288
285 289 @templatefilter(b'isodate', intype=templateutil.date)
286 290 def isodate(text):
287 291 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
288 292 +0200".
289 293 """
290 294 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
291 295
292 296
293 297 @templatefilter(b'isodatesec', intype=templateutil.date)
294 298 def isodatesec(text):
295 299 """Date. Returns the date in ISO 8601 format, including
296 300 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
297 301 filter.
298 302 """
299 303 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
300 304
301 305
302 306 def indent(text, prefix, firstline=b''):
303 307 '''indent each non-empty line of text after first with prefix.'''
304 308 lines = text.splitlines()
305 309 num_lines = len(lines)
306 310 endswithnewline = text[-1:] == b'\n'
307 311
308 312 def indenter():
309 313 for i in pycompat.xrange(num_lines):
310 314 l = lines[i]
311 315 if l.strip():
312 316 yield prefix if i else firstline
313 317 yield l
314 318 if i < num_lines - 1 or endswithnewline:
315 319 yield b'\n'
316 320
317 321 return b"".join(indenter())
318 322
319 323
320 324 @templatefilter(b'json')
321 325 def json(obj, paranoid=True):
322 326 """Any object. Serializes the object to a JSON formatted text."""
323 327 if obj is None:
324 328 return b'null'
325 329 elif obj is False:
326 330 return b'false'
327 331 elif obj is True:
328 332 return b'true'
329 333 elif isinstance(obj, (int, pycompat.long, float)):
330 334 return pycompat.bytestr(obj)
331 335 elif isinstance(obj, bytes):
332 336 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
333 337 elif isinstance(obj, type(u'')):
334 338 raise error.ProgrammingError(
335 339 b'Mercurial only does output with bytes: %r' % obj
336 340 )
337 341 elif util.safehasattr(obj, b'keys'):
338 342 out = [
339 343 b'"%s": %s'
340 344 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
341 345 for k, v in sorted(pycompat.iteritems(obj))
342 346 ]
343 347 return b'{' + b', '.join(out) + b'}'
344 348 elif util.safehasattr(obj, b'__iter__'):
345 349 out = [json(i, paranoid) for i in obj]
346 350 return b'[' + b', '.join(out) + b']'
347 351 raise error.ProgrammingError(b'cannot encode %r' % obj)
348 352
349 353
350 354 @templatefilter(b'lower', intype=bytes)
351 355 def lower(text):
352 356 """Any text. Converts the text to lowercase."""
353 357 return encoding.lower(text)
354 358
355 359
356 360 @templatefilter(b'nonempty', intype=bytes)
357 361 def nonempty(text):
358 362 """Any text. Returns '(none)' if the string is empty."""
359 363 return text or b"(none)"
360 364
361 365
362 366 @templatefilter(b'obfuscate', intype=bytes)
363 367 def obfuscate(text):
364 368 """Any text. Returns the input text rendered as a sequence of
365 369 XML entities.
366 370 """
367 371 text = pycompat.unicode(
368 372 text, pycompat.sysstr(encoding.encoding), r'replace'
369 373 )
370 374 return b''.join([b'&#%d;' % ord(c) for c in text])
371 375
372 376
373 377 @templatefilter(b'permissions', intype=bytes)
374 378 def permissions(flags):
375 379 if b"l" in flags:
376 380 return b"lrwxrwxrwx"
377 381 if b"x" in flags:
378 382 return b"-rwxr-xr-x"
379 383 return b"-rw-r--r--"
380 384
381 385
382 386 @templatefilter(b'person', intype=bytes)
383 387 def person(author):
384 388 """Any text. Returns the name before an email address,
385 389 interpreting it as per RFC 5322.
386 390 """
387 391 return stringutil.person(author)
388 392
389 393
390 394 @templatefilter(b'revescape', intype=bytes)
391 395 def revescape(text):
392 396 """Any text. Escapes all "special" characters, except @.
393 397 Forward slashes are escaped twice to prevent web servers from prematurely
394 398 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
395 399 """
396 400 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
397 401
398 402
399 403 @templatefilter(b'rfc3339date', intype=templateutil.date)
400 404 def rfc3339date(text):
401 405 """Date. Returns a date using the Internet date format
402 406 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
403 407 """
404 408 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
405 409
406 410
407 411 @templatefilter(b'rfc822date', intype=templateutil.date)
408 412 def rfc822date(text):
409 413 """Date. Returns a date using the same format used in email
410 414 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
411 415 """
412 416 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
413 417
414 418
415 419 @templatefilter(b'short', intype=bytes)
416 420 def short(text):
417 421 """Changeset hash. Returns the short form of a changeset hash,
418 422 i.e. a 12 hexadecimal digit string.
419 423 """
420 424 return text[:12]
421 425
422 426
423 427 @templatefilter(b'shortbisect', intype=bytes)
424 428 def shortbisect(label):
425 429 """Any text. Treats `label` as a bisection status, and
426 430 returns a single-character representing the status (G: good, B: bad,
427 431 S: skipped, U: untested, I: ignored). Returns single space if `text`
428 432 is not a valid bisection status.
429 433 """
430 434 if label:
431 435 return label[0:1].upper()
432 436 return b' '
433 437
434 438
435 439 @templatefilter(b'shortdate', intype=templateutil.date)
436 440 def shortdate(text):
437 441 """Date. Returns a date like "2006-09-18"."""
438 442 return dateutil.shortdate(text)
439 443
440 444
441 445 @templatefilter(b'slashpath', intype=bytes)
442 446 def slashpath(path):
443 447 """Any text. Replaces the native path separator with slash."""
444 448 return util.pconvert(path)
445 449
446 450
447 451 @templatefilter(b'splitlines', intype=bytes)
448 452 def splitlines(text):
449 453 """Any text. Split text into a list of lines."""
450 454 return templateutil.hybridlist(text.splitlines(), name=b'line')
451 455
452 456
453 457 @templatefilter(b'stringescape', intype=bytes)
454 458 def stringescape(text):
455 459 return stringutil.escapestr(text)
456 460
457 461
458 462 @templatefilter(b'stringify', intype=bytes)
459 463 def stringify(thing):
460 464 """Any type. Turns the value into text by converting values into
461 465 text and concatenating them.
462 466 """
463 467 return thing # coerced by the intype
464 468
465 469
466 470 @templatefilter(b'stripdir', intype=bytes)
467 471 def stripdir(text):
468 472 """Treat the text as path and strip a directory level, if
469 473 possible. For example, "foo" and "foo/bar" becomes "foo".
470 474 """
471 475 dir = os.path.dirname(text)
472 476 if dir == b"":
473 477 return os.path.basename(text)
474 478 else:
475 479 return dir
476 480
477 481
478 482 @templatefilter(b'tabindent', intype=bytes)
479 483 def tabindent(text):
480 484 """Any text. Returns the text, with every non-empty line
481 485 except the first starting with a tab character.
482 486 """
483 487 return indent(text, b'\t')
484 488
485 489
486 490 @templatefilter(b'upper', intype=bytes)
487 491 def upper(text):
488 492 """Any text. Converts the text to uppercase."""
489 493 return encoding.upper(text)
490 494
491 495
492 496 @templatefilter(b'urlescape', intype=bytes)
493 497 def urlescape(text):
494 498 """Any text. Escapes all "special" characters. For example,
495 499 "foo bar" becomes "foo%20bar".
496 500 """
497 501 return urlreq.quote(text)
498 502
499 503
500 504 @templatefilter(b'user', intype=bytes)
501 505 def userfilter(text):
502 506 """Any text. Returns a short representation of a user name or email
503 507 address."""
504 508 return stringutil.shortuser(text)
505 509
506 510
507 511 @templatefilter(b'emailuser', intype=bytes)
508 512 def emailuser(text):
509 513 """Any text. Returns the user portion of an email address."""
510 514 return stringutil.emailuser(text)
511 515
512 516
513 517 @templatefilter(b'utf8', intype=bytes)
514 518 def utf8(text):
515 519 """Any text. Converts from the local character encoding to UTF-8."""
516 520 return encoding.fromlocal(text)
517 521
518 522
519 523 @templatefilter(b'xmlescape', intype=bytes)
520 524 def xmlescape(text):
521 525 text = (
522 526 text.replace(b'&', b'&amp;')
523 527 .replace(b'<', b'&lt;')
524 528 .replace(b'>', b'&gt;')
525 529 .replace(b'"', b'&quot;')
526 530 .replace(b"'", b'&#39;')
527 531 ) # &apos; invalid in HTML
528 532 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
529 533
530 534
531 535 def websub(text, websubtable):
532 536 """:websub: Any text. Only applies to hgweb. Applies the regular
533 537 expression replacements defined in the websub section.
534 538 """
535 539 if websubtable:
536 540 for regexp, format in websubtable:
537 541 text = regexp.sub(format, text)
538 542 return text
539 543
540 544
541 545 def loadfilter(ui, extname, registrarobj):
542 546 """Load template filter from specified registrarobj
543 547 """
544 548 for name, func in pycompat.iteritems(registrarobj._table):
545 549 filters[name] = func
546 550
547 551
548 552 # tell hggettext to extract docstrings from these functions:
549 553 i18nfunctions = filters.values()
@@ -1,1165 +1,1165 b''
1 1 # templateutil.py - utility for template evaluation
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import abc
11 11 import types
12 12
13 13 from .i18n import _
14 14 from .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 417 """
418 418
419 419 def __init__(self, repo, revs, name=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
425 425 def contains(self, context, mapping, item):
426 426 rev = unwrapinteger(context, mapping, item)
427 427 return rev in self._revs
428 428
429 429 def getmember(self, context, mapping, key):
430 430 raise error.ParseError(_(b'not a dictionary'))
431 431
432 432 def getmin(self, context, mapping):
433 433 makehybriditem = self._makehybriditemfunc()
434 434 return makehybriditem(self._revs.min())
435 435
436 436 def getmax(self, context, mapping):
437 437 makehybriditem = self._makehybriditemfunc()
438 438 return makehybriditem(self._revs.max())
439 439
440 440 def filter(self, context, mapping, select):
441 441 makehybriditem = self._makehybriditemfunc()
442 442 frevs = self._revs.filter(lambda r: select(makehybriditem(r)))
443 443 # once filtered, no need to support old-style list template
444 444 return revslist(self._repo, frevs, name=None)
445 445
446 446 def itermaps(self, context):
447 447 makemap = self._makemapfunc()
448 448 for r in self._revs:
449 449 yield makemap(r)
450 450
451 451 def _makehybriditemfunc(self):
452 452 makemap = self._makemapfunc()
453 453 return lambda r: hybriditem(None, r, r, makemap)
454 454
455 455 def _makemapfunc(self):
456 456 repo = self._repo
457 457 name = self._name
458 458 if name:
459 459 return lambda r: {name: r, b'ctx': repo[r]}
460 460 else:
461 461 return lambda r: {b'ctx': repo[r]}
462 462
463 463 def join(self, context, mapping, sep):
464 464 return joinitems(self._revs, sep)
465 465
466 466 def show(self, context, mapping):
467 467 if self._name:
468 468 srevs = [b'%d' % r for r in self._revs]
469 469 return _showcompatlist(context, mapping, self._name, srevs)
470 470 else:
471 471 return self.join(context, mapping, b' ')
472 472
473 473 def tobool(self, context, mapping):
474 474 return bool(self._revs)
475 475
476 476 def tovalue(self, context, mapping):
477 return list(self._revs)
477 return self._revs
478 478
479 479
480 480 class _mappingsequence(wrapped):
481 481 """Wrapper for sequence of template mappings
482 482
483 483 This represents an inner template structure (i.e. a list of dicts),
484 484 which can also be rendered by the specified named/literal template.
485 485
486 486 Template mappings may be nested.
487 487 """
488 488
489 489 def __init__(self, name=None, tmpl=None, sep=b''):
490 490 if name is not None and tmpl is not None:
491 491 raise error.ProgrammingError(
492 492 b'name and tmpl are mutually exclusive'
493 493 )
494 494 self._name = name
495 495 self._tmpl = tmpl
496 496 self._defaultsep = sep
497 497
498 498 def contains(self, context, mapping, item):
499 499 raise error.ParseError(_(b'not comparable'))
500 500
501 501 def getmember(self, context, mapping, key):
502 502 raise error.ParseError(_(b'not a dictionary'))
503 503
504 504 def getmin(self, context, mapping):
505 505 raise error.ParseError(_(b'not comparable'))
506 506
507 507 def getmax(self, context, mapping):
508 508 raise error.ParseError(_(b'not comparable'))
509 509
510 510 def filter(self, context, mapping, select):
511 511 # implement if necessary; we'll need a wrapped type for a mapping dict
512 512 raise error.ParseError(_(b'not filterable without template'))
513 513
514 514 def join(self, context, mapping, sep):
515 515 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
516 516 if self._name:
517 517 itemiter = (context.process(self._name, m) for m in mapsiter)
518 518 elif self._tmpl:
519 519 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
520 520 else:
521 521 raise error.ParseError(_(b'not displayable without template'))
522 522 return joinitems(itemiter, sep)
523 523
524 524 def show(self, context, mapping):
525 525 return self.join(context, mapping, self._defaultsep)
526 526
527 527 def tovalue(self, context, mapping):
528 528 knownres = context.knownresourcekeys()
529 529 items = []
530 530 for nm in self.itermaps(context):
531 531 # drop internal resources (recursively) which shouldn't be displayed
532 532 lm = context.overlaymap(mapping, nm)
533 533 items.append(
534 534 {
535 535 k: unwrapvalue(context, lm, v)
536 536 for k, v in pycompat.iteritems(nm)
537 537 if k not in knownres
538 538 }
539 539 )
540 540 return items
541 541
542 542
543 543 class mappinggenerator(_mappingsequence):
544 544 """Wrapper for generator of template mappings
545 545
546 546 The function ``make(context, *args)`` should return a generator of
547 547 mapping dicts.
548 548 """
549 549
550 550 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
551 551 super(mappinggenerator, self).__init__(name, tmpl, sep)
552 552 self._make = make
553 553 self._args = args
554 554
555 555 def itermaps(self, context):
556 556 return self._make(context, *self._args)
557 557
558 558 def tobool(self, context, mapping):
559 559 return _nonempty(self.itermaps(context))
560 560
561 561
562 562 class mappinglist(_mappingsequence):
563 563 """Wrapper for list of template mappings"""
564 564
565 565 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
566 566 super(mappinglist, self).__init__(name, tmpl, sep)
567 567 self._mappings = mappings
568 568
569 569 def itermaps(self, context):
570 570 return iter(self._mappings)
571 571
572 572 def tobool(self, context, mapping):
573 573 return bool(self._mappings)
574 574
575 575
576 576 class mappingdict(mappable, _mappingsequence):
577 577 """Wrapper for a single template mapping
578 578
579 579 This isn't a sequence in a way that the underlying dict won't be iterated
580 580 as a dict, but shares most of the _mappingsequence functions.
581 581 """
582 582
583 583 def __init__(self, mapping, name=None, tmpl=None):
584 584 super(mappingdict, self).__init__(name, tmpl)
585 585 self._mapping = mapping
586 586
587 587 def tomap(self, context):
588 588 return self._mapping
589 589
590 590 def tobool(self, context, mapping):
591 591 # no idea when a template mapping should be considered an empty, but
592 592 # a mapping dict should have at least one item in practice, so always
593 593 # mark this as non-empty.
594 594 return True
595 595
596 596 def tovalue(self, context, mapping):
597 597 return super(mappingdict, self).tovalue(context, mapping)[0]
598 598
599 599
600 600 class mappingnone(wrappedvalue):
601 601 """Wrapper for None, but supports map operation
602 602
603 603 This represents None of Optional[mappable]. It's similar to
604 604 mapplinglist([]), but the underlying value is not [], but None.
605 605 """
606 606
607 607 def __init__(self):
608 608 super(mappingnone, self).__init__(None)
609 609
610 610 def itermaps(self, context):
611 611 return iter([])
612 612
613 613
614 614 class mappedgenerator(wrapped):
615 615 """Wrapper for generator of strings which acts as a list
616 616
617 617 The function ``make(context, *args)`` should return a generator of
618 618 byte strings, or a generator of (possibly nested) generators of byte
619 619 strings (i.e. a generator for a list of byte strings.)
620 620 """
621 621
622 622 def __init__(self, make, args=()):
623 623 self._make = make
624 624 self._args = args
625 625
626 626 def contains(self, context, mapping, item):
627 627 item = stringify(context, mapping, item)
628 628 return item in self.tovalue(context, mapping)
629 629
630 630 def _gen(self, context):
631 631 return self._make(context, *self._args)
632 632
633 633 def getmember(self, context, mapping, key):
634 634 raise error.ParseError(_(b'not a dictionary'))
635 635
636 636 def getmin(self, context, mapping):
637 637 return self._getby(context, mapping, min)
638 638
639 639 def getmax(self, context, mapping):
640 640 return self._getby(context, mapping, max)
641 641
642 642 def _getby(self, context, mapping, func):
643 643 xs = self.tovalue(context, mapping)
644 644 if not xs:
645 645 raise error.ParseError(_(b'empty sequence'))
646 646 return func(xs)
647 647
648 648 @staticmethod
649 649 def _filteredgen(context, mapping, make, args, select):
650 650 for x in make(context, *args):
651 651 s = stringify(context, mapping, x)
652 652 if select(wrappedbytes(s)):
653 653 yield s
654 654
655 655 def filter(self, context, mapping, select):
656 656 args = (mapping, self._make, self._args, select)
657 657 return mappedgenerator(self._filteredgen, args)
658 658
659 659 def itermaps(self, context):
660 660 raise error.ParseError(_(b'list of strings is not mappable'))
661 661
662 662 def join(self, context, mapping, sep):
663 663 return joinitems(self._gen(context), sep)
664 664
665 665 def show(self, context, mapping):
666 666 return self.join(context, mapping, b'')
667 667
668 668 def tobool(self, context, mapping):
669 669 return _nonempty(self._gen(context))
670 670
671 671 def tovalue(self, context, mapping):
672 672 return [stringify(context, mapping, x) for x in self._gen(context)]
673 673
674 674
675 675 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
676 676 """Wrap data to support both dict-like and string-like operations"""
677 677 prefmt = pycompat.identity
678 678 if fmt is None:
679 679 fmt = b'%s=%s'
680 680 prefmt = pycompat.bytestr
681 681 return hybrid(
682 682 gen,
683 683 data,
684 684 lambda k: {key: k, value: data[k]},
685 685 lambda k: fmt % (prefmt(k), prefmt(data[k])),
686 686 )
687 687
688 688
689 689 def hybridlist(data, name, fmt=None, gen=None):
690 690 """Wrap data to support both list-like and string-like operations"""
691 691 prefmt = pycompat.identity
692 692 if fmt is None:
693 693 fmt = b'%s'
694 694 prefmt = pycompat.bytestr
695 695 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
696 696
697 697
698 698 def compatdict(
699 699 context,
700 700 mapping,
701 701 name,
702 702 data,
703 703 key=b'key',
704 704 value=b'value',
705 705 fmt=None,
706 706 plural=None,
707 707 separator=b' ',
708 708 ):
709 709 """Wrap data like hybriddict(), but also supports old-style list template
710 710
711 711 This exists for backward compatibility with the old-style template. Use
712 712 hybriddict() for new template keywords.
713 713 """
714 714 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
715 715 f = _showcompatlist(context, mapping, name, c, plural, separator)
716 716 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
717 717
718 718
719 719 def compatlist(
720 720 context,
721 721 mapping,
722 722 name,
723 723 data,
724 724 element=None,
725 725 fmt=None,
726 726 plural=None,
727 727 separator=b' ',
728 728 ):
729 729 """Wrap data like hybridlist(), but also supports old-style list template
730 730
731 731 This exists for backward compatibility with the old-style template. Use
732 732 hybridlist() for new template keywords.
733 733 """
734 734 f = _showcompatlist(context, mapping, name, data, plural, separator)
735 735 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
736 736
737 737
738 738 def compatfilecopiesdict(context, mapping, name, copies):
739 739 """Wrap list of (dest, source) file names to support old-style list
740 740 template and field names
741 741
742 742 This exists for backward compatibility. Use hybriddict for new template
743 743 keywords.
744 744 """
745 745 # no need to provide {path} to old-style list template
746 746 c = [{b'name': k, b'source': v} for k, v in copies]
747 747 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
748 748 copies = util.sortdict(copies)
749 749 return hybrid(
750 750 f,
751 751 copies,
752 752 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
753 753 lambda k: b'%s (%s)' % (k, copies[k]),
754 754 )
755 755
756 756
757 757 def compatfileslist(context, mapping, name, files):
758 758 """Wrap list of file names to support old-style list template and field
759 759 names
760 760
761 761 This exists for backward compatibility. Use hybridlist for new template
762 762 keywords.
763 763 """
764 764 f = _showcompatlist(context, mapping, name, files)
765 765 return hybrid(
766 766 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
767 767 )
768 768
769 769
770 770 def _showcompatlist(
771 771 context, mapping, name, values, plural=None, separator=b' '
772 772 ):
773 773 """Return a generator that renders old-style list template
774 774
775 775 name is name of key in template map.
776 776 values is list of strings or dicts.
777 777 plural is plural of name, if not simply name + 's'.
778 778 separator is used to join values as a string
779 779
780 780 expansion works like this, given name 'foo'.
781 781
782 782 if values is empty, expand 'no_foos'.
783 783
784 784 if 'foo' not in template map, return values as a string,
785 785 joined by 'separator'.
786 786
787 787 expand 'start_foos'.
788 788
789 789 for each value, expand 'foo'. if 'last_foo' in template
790 790 map, expand it instead of 'foo' for last key.
791 791
792 792 expand 'end_foos'.
793 793 """
794 794 if not plural:
795 795 plural = name + b's'
796 796 if not values:
797 797 noname = b'no_' + plural
798 798 if context.preload(noname):
799 799 yield context.process(noname, mapping)
800 800 return
801 801 if not context.preload(name):
802 802 if isinstance(values[0], bytes):
803 803 yield separator.join(values)
804 804 else:
805 805 for v in values:
806 806 r = dict(v)
807 807 r.update(mapping)
808 808 yield r
809 809 return
810 810 startname = b'start_' + plural
811 811 if context.preload(startname):
812 812 yield context.process(startname, mapping)
813 813
814 814 def one(v, tag=name):
815 815 vmapping = {}
816 816 try:
817 817 vmapping.update(v)
818 818 # Python 2 raises ValueError if the type of v is wrong. Python
819 819 # 3 raises TypeError.
820 820 except (AttributeError, TypeError, ValueError):
821 821 try:
822 822 # Python 2 raises ValueError trying to destructure an e.g.
823 823 # bytes. Python 3 raises TypeError.
824 824 for a, b in v:
825 825 vmapping[a] = b
826 826 except (TypeError, ValueError):
827 827 vmapping[name] = v
828 828 vmapping = context.overlaymap(mapping, vmapping)
829 829 return context.process(tag, vmapping)
830 830
831 831 lastname = b'last_' + name
832 832 if context.preload(lastname):
833 833 last = values.pop()
834 834 else:
835 835 last = None
836 836 for v in values:
837 837 yield one(v)
838 838 if last is not None:
839 839 yield one(last, tag=lastname)
840 840 endname = b'end_' + plural
841 841 if context.preload(endname):
842 842 yield context.process(endname, mapping)
843 843
844 844
845 845 def flatten(context, mapping, thing):
846 846 """Yield a single stream from a possibly nested set of iterators"""
847 847 if isinstance(thing, wrapped):
848 848 thing = thing.show(context, mapping)
849 849 if isinstance(thing, bytes):
850 850 yield thing
851 851 elif isinstance(thing, str):
852 852 # We can only hit this on Python 3, and it's here to guard
853 853 # against infinite recursion.
854 854 raise error.ProgrammingError(
855 855 b'Mercurial IO including templates is done'
856 856 b' with bytes, not strings, got %r' % thing
857 857 )
858 858 elif thing is None:
859 859 pass
860 860 elif not util.safehasattr(thing, b'__iter__'):
861 861 yield pycompat.bytestr(thing)
862 862 else:
863 863 for i in thing:
864 864 if isinstance(i, wrapped):
865 865 i = i.show(context, mapping)
866 866 if isinstance(i, bytes):
867 867 yield i
868 868 elif i is None:
869 869 pass
870 870 elif not util.safehasattr(i, b'__iter__'):
871 871 yield pycompat.bytestr(i)
872 872 else:
873 873 for j in flatten(context, mapping, i):
874 874 yield j
875 875
876 876
877 877 def stringify(context, mapping, thing):
878 878 """Turn values into bytes by converting into text and concatenating them"""
879 879 if isinstance(thing, bytes):
880 880 return thing # retain localstr to be round-tripped
881 881 return b''.join(flatten(context, mapping, thing))
882 882
883 883
884 884 def findsymbolicname(arg):
885 885 """Find symbolic name for the given compiled expression; returns None
886 886 if nothing found reliably"""
887 887 while True:
888 888 func, data = arg
889 889 if func is runsymbol:
890 890 return data
891 891 elif func is runfilter:
892 892 arg = data[0]
893 893 else:
894 894 return None
895 895
896 896
897 897 def _nonempty(xiter):
898 898 try:
899 899 next(xiter)
900 900 return True
901 901 except StopIteration:
902 902 return False
903 903
904 904
905 905 def _unthunk(context, mapping, thing):
906 906 """Evaluate a lazy byte string into value"""
907 907 if not isinstance(thing, types.GeneratorType):
908 908 return thing
909 909 return stringify(context, mapping, thing)
910 910
911 911
912 912 def evalrawexp(context, mapping, arg):
913 913 """Evaluate given argument as a bare template object which may require
914 914 further processing (such as folding generator of strings)"""
915 915 func, data = arg
916 916 return func(context, mapping, data)
917 917
918 918
919 919 def evalwrapped(context, mapping, arg):
920 920 """Evaluate given argument to wrapped object"""
921 921 thing = evalrawexp(context, mapping, arg)
922 922 return makewrapped(context, mapping, thing)
923 923
924 924
925 925 def makewrapped(context, mapping, thing):
926 926 """Lift object to a wrapped type"""
927 927 if isinstance(thing, wrapped):
928 928 return thing
929 929 thing = _unthunk(context, mapping, thing)
930 930 if isinstance(thing, bytes):
931 931 return wrappedbytes(thing)
932 932 return wrappedvalue(thing)
933 933
934 934
935 935 def evalfuncarg(context, mapping, arg):
936 936 """Evaluate given argument as value type"""
937 937 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
938 938
939 939
940 940 def unwrapvalue(context, mapping, thing):
941 941 """Move the inner value object out of the wrapper"""
942 942 if isinstance(thing, wrapped):
943 943 return thing.tovalue(context, mapping)
944 944 # evalrawexp() may return string, generator of strings or arbitrary object
945 945 # such as date tuple, but filter does not want generator.
946 946 return _unthunk(context, mapping, thing)
947 947
948 948
949 949 def evalboolean(context, mapping, arg):
950 950 """Evaluate given argument as boolean, but also takes boolean literals"""
951 951 func, data = arg
952 952 if func is runsymbol:
953 953 thing = func(context, mapping, data, default=None)
954 954 if thing is None:
955 955 # not a template keyword, takes as a boolean literal
956 956 thing = stringutil.parsebool(data)
957 957 else:
958 958 thing = func(context, mapping, data)
959 959 return makewrapped(context, mapping, thing).tobool(context, mapping)
960 960
961 961
962 962 def evaldate(context, mapping, arg, err=None):
963 963 """Evaluate given argument as a date tuple or a date string; returns
964 964 a (unixtime, offset) tuple"""
965 965 thing = evalrawexp(context, mapping, arg)
966 966 return unwrapdate(context, mapping, thing, err)
967 967
968 968
969 969 def unwrapdate(context, mapping, thing, err=None):
970 970 if isinstance(thing, date):
971 971 return thing.tovalue(context, mapping)
972 972 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
973 973 thing = unwrapvalue(context, mapping, thing)
974 974 try:
975 975 return dateutil.parsedate(thing)
976 976 except AttributeError:
977 977 raise error.ParseError(err or _(b'not a date tuple nor a string'))
978 978 except error.ParseError:
979 979 if not err:
980 980 raise
981 981 raise error.ParseError(err)
982 982
983 983
984 984 def evalinteger(context, mapping, arg, err=None):
985 985 thing = evalrawexp(context, mapping, arg)
986 986 return unwrapinteger(context, mapping, thing, err)
987 987
988 988
989 989 def unwrapinteger(context, mapping, thing, err=None):
990 990 thing = unwrapvalue(context, mapping, thing)
991 991 try:
992 992 return int(thing)
993 993 except (TypeError, ValueError):
994 994 raise error.ParseError(err or _(b'not an integer'))
995 995
996 996
997 997 def evalstring(context, mapping, arg):
998 998 return stringify(context, mapping, evalrawexp(context, mapping, arg))
999 999
1000 1000
1001 1001 def evalstringliteral(context, mapping, arg):
1002 1002 """Evaluate given argument as string template, but returns symbol name
1003 1003 if it is unknown"""
1004 1004 func, data = arg
1005 1005 if func is runsymbol:
1006 1006 thing = func(context, mapping, data, default=data)
1007 1007 else:
1008 1008 thing = func(context, mapping, data)
1009 1009 return stringify(context, mapping, thing)
1010 1010
1011 1011
1012 1012 _unwrapfuncbytype = {
1013 1013 None: unwrapvalue,
1014 1014 bytes: stringify,
1015 1015 date: unwrapdate,
1016 1016 int: unwrapinteger,
1017 1017 }
1018 1018
1019 1019
1020 1020 def unwrapastype(context, mapping, thing, typ):
1021 1021 """Move the inner value object out of the wrapper and coerce its type"""
1022 1022 try:
1023 1023 f = _unwrapfuncbytype[typ]
1024 1024 except KeyError:
1025 1025 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
1026 1026 return f(context, mapping, thing)
1027 1027
1028 1028
1029 1029 def runinteger(context, mapping, data):
1030 1030 return int(data)
1031 1031
1032 1032
1033 1033 def runstring(context, mapping, data):
1034 1034 return data
1035 1035
1036 1036
1037 1037 def _recursivesymbolblocker(key):
1038 1038 def showrecursion(context, mapping):
1039 1039 raise error.Abort(_(b"recursive reference '%s' in template") % key)
1040 1040
1041 1041 return showrecursion
1042 1042
1043 1043
1044 1044 def runsymbol(context, mapping, key, default=b''):
1045 1045 v = context.symbol(mapping, key)
1046 1046 if v is None:
1047 1047 # put poison to cut recursion. we can't move this to parsing phase
1048 1048 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
1049 1049 safemapping = mapping.copy()
1050 1050 safemapping[key] = _recursivesymbolblocker(key)
1051 1051 try:
1052 1052 v = context.process(key, safemapping)
1053 1053 except TemplateNotFound:
1054 1054 v = default
1055 1055 if callable(v):
1056 1056 # new templatekw
1057 1057 try:
1058 1058 return v(context, mapping)
1059 1059 except ResourceUnavailable:
1060 1060 # unsupported keyword is mapped to empty just like unknown keyword
1061 1061 return None
1062 1062 return v
1063 1063
1064 1064
1065 1065 def runtemplate(context, mapping, template):
1066 1066 for arg in template:
1067 1067 yield evalrawexp(context, mapping, arg)
1068 1068
1069 1069
1070 1070 def runfilter(context, mapping, data):
1071 1071 arg, filt = data
1072 1072 thing = evalrawexp(context, mapping, arg)
1073 1073 intype = getattr(filt, '_intype', None)
1074 1074 try:
1075 1075 thing = unwrapastype(context, mapping, thing, intype)
1076 1076 return filt(thing)
1077 1077 except error.ParseError as e:
1078 1078 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1079 1079
1080 1080
1081 1081 def _formatfiltererror(arg, filt):
1082 1082 fn = pycompat.sysbytes(filt.__name__)
1083 1083 sym = findsymbolicname(arg)
1084 1084 if not sym:
1085 1085 return _(b"incompatible use of template filter '%s'") % fn
1086 1086 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1087 1087 fn,
1088 1088 sym,
1089 1089 )
1090 1090
1091 1091
1092 1092 def _iteroverlaymaps(context, origmapping, newmappings):
1093 1093 """Generate combined mappings from the original mapping and an iterable
1094 1094 of partial mappings to override the original"""
1095 1095 for i, nm in enumerate(newmappings):
1096 1096 lm = context.overlaymap(origmapping, nm)
1097 1097 lm[b'index'] = i
1098 1098 yield lm
1099 1099
1100 1100
1101 1101 def _applymap(context, mapping, d, darg, targ):
1102 1102 try:
1103 1103 diter = d.itermaps(context)
1104 1104 except error.ParseError as err:
1105 1105 sym = findsymbolicname(darg)
1106 1106 if not sym:
1107 1107 raise
1108 1108 hint = _(b"keyword '%s' does not support map operation") % sym
1109 1109 raise error.ParseError(bytes(err), hint=hint)
1110 1110 for lm in _iteroverlaymaps(context, mapping, diter):
1111 1111 yield evalrawexp(context, lm, targ)
1112 1112
1113 1113
1114 1114 def runmap(context, mapping, data):
1115 1115 darg, targ = data
1116 1116 d = evalwrapped(context, mapping, darg)
1117 1117 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1118 1118
1119 1119
1120 1120 def runmember(context, mapping, data):
1121 1121 darg, memb = data
1122 1122 d = evalwrapped(context, mapping, darg)
1123 1123 if isinstance(d, mappable):
1124 1124 lm = context.overlaymap(mapping, d.tomap(context))
1125 1125 return runsymbol(context, lm, memb)
1126 1126 try:
1127 1127 return d.getmember(context, mapping, memb)
1128 1128 except error.ParseError as err:
1129 1129 sym = findsymbolicname(darg)
1130 1130 if not sym:
1131 1131 raise
1132 1132 hint = _(b"keyword '%s' does not support member operation") % sym
1133 1133 raise error.ParseError(bytes(err), hint=hint)
1134 1134
1135 1135
1136 1136 def runnegate(context, mapping, data):
1137 1137 data = evalinteger(
1138 1138 context, mapping, data, _(b'negation needs an integer argument')
1139 1139 )
1140 1140 return -data
1141 1141
1142 1142
1143 1143 def runarithmetic(context, mapping, data):
1144 1144 func, left, right = data
1145 1145 left = evalinteger(
1146 1146 context, mapping, left, _(b'arithmetic only defined on integers')
1147 1147 )
1148 1148 right = evalinteger(
1149 1149 context, mapping, right, _(b'arithmetic only defined on integers')
1150 1150 )
1151 1151 try:
1152 1152 return func(left, right)
1153 1153 except ZeroDivisionError:
1154 1154 raise error.Abort(_(b'division by zero is not defined'))
1155 1155
1156 1156
1157 1157 def joinitems(itemiter, sep):
1158 1158 """Join items with the separator; Returns generator of bytes"""
1159 1159 first = True
1160 1160 for x in itemiter:
1161 1161 if first:
1162 1162 first = False
1163 1163 elif sep:
1164 1164 yield sep
1165 1165 yield x
General Comments 0
You need to be logged in to leave comments. Login now