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