##// END OF EJS Templates
typing: add a few trivial type hints to `mercurial/templater.py`...
Matt Harbison -
r52571:0e16efe3 default
parent child Browse files
Show More
@@ -1,1163 +1,1173
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Slightly complicated template engine for commands and hgweb
9 9
10 10 This module provides low-level interface to the template engine. See the
11 11 formatter and cmdutil modules if you are looking for high-level functions
12 12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13 13
14 14 Internal Data Types
15 15 -------------------
16 16
17 17 Template keywords and functions take a dictionary of current symbols and
18 18 resources (a "mapping") and return result. Inputs and outputs must be one
19 19 of the following data types:
20 20
21 21 bytes
22 22 a byte string, which is generally a human-readable text in local encoding.
23 23
24 24 generator
25 25 a lazily-evaluated byte string, which is a possibly nested generator of
26 26 values of any printable types, and will be folded by ``stringify()``
27 27 or ``flatten()``.
28 28
29 29 None
30 30 sometimes represents an empty value, which can be stringified to ''.
31 31
32 32 True, False, int, float
33 33 can be stringified as such.
34 34
35 35 wrappedbytes, wrappedvalue
36 36 a wrapper for the above printable types.
37 37
38 38 date
39 39 represents a (unixtime, offset) tuple.
40 40
41 41 hybrid
42 42 represents a list/dict of printable values, which can also be converted
43 43 to mappings by % operator.
44 44
45 45 hybriditem
46 46 represents a scalar printable value, also supports % operator.
47 47
48 48 revslist
49 49 represents a list of revision numbers.
50 50
51 51 mappinggenerator, mappinglist
52 52 represents mappings (i.e. a list of dicts), which may have default
53 53 output format.
54 54
55 55 mappingdict
56 56 represents a single mapping (i.e. a dict), which may have default output
57 57 format.
58 58
59 59 mappingnone
60 60 represents None of Optional[mappable], which will be mapped to an empty
61 61 string by % operation.
62 62
63 63 mappedgenerator
64 64 a lazily-evaluated list of byte strings, which is e.g. a result of %
65 65 operation.
66 66 """
67 67
68 68
69 69 import abc
70 70 import os
71 71
72 from typing import (
73 BinaryIO,
74 Optional,
75 Tuple,
76 )
77
72 78 from .i18n import _
73 79 from .pycompat import (
74 80 FileNotFoundError,
75 81 )
76 82 from . import (
77 83 config,
78 84 encoding,
79 85 error,
80 86 parser,
81 87 pycompat,
82 88 templatefilters,
83 89 templatefuncs,
84 90 templateutil,
85 91 util,
86 92 )
87 93 from .utils import (
88 94 resourceutil,
89 95 stringutil,
90 96 )
91 97
92 98 # template parsing
93 99
94 100 elements = {
95 101 # token-type: binding-strength, primary, prefix, infix, suffix
96 102 b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
97 103 b".": (18, None, None, (b".", 18), None),
98 104 b"%": (15, None, None, (b"%", 15), None),
99 105 b"|": (15, None, None, (b"|", 15), None),
100 106 b"*": (5, None, None, (b"*", 5), None),
101 107 b"/": (5, None, None, (b"/", 5), None),
102 108 b"+": (4, None, None, (b"+", 4), None),
103 109 b"-": (4, None, (b"negate", 19), (b"-", 4), None),
104 110 b"=": (3, None, None, (b"keyvalue", 3), None),
105 111 b",": (2, None, None, (b"list", 2), None),
106 112 b")": (0, None, None, None, None),
107 113 b"integer": (0, b"integer", None, None, None),
108 114 b"symbol": (0, b"symbol", None, None, None),
109 115 b"string": (0, b"string", None, None, None),
110 116 b"template": (0, b"template", None, None, None),
111 117 b"end": (0, None, None, None, None),
112 118 }
113 119
114 120
115 121 def tokenize(program, start, end, term=None):
116 122 """Parse a template expression into a stream of tokens, which must end
117 123 with term if specified"""
118 124 pos = start
119 125 program = pycompat.bytestr(program)
120 126 while pos < end:
121 127 c = program[pos]
122 128 if c.isspace(): # skip inter-token whitespace
123 129 pass
124 130 elif c in b"(=,).%|+-*/": # handle simple operators
125 131 yield (c, None, pos)
126 132 elif c in b'"\'': # handle quoted templates
127 133 s = pos + 1
128 134 data, pos = _parsetemplate(program, s, end, c)
129 135 yield (b'template', data, s)
130 136 pos -= 1
131 137 elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
132 138 # handle quoted strings
133 139 c = program[pos + 1]
134 140 s = pos = pos + 2
135 141 while pos < end: # find closing quote
136 142 d = program[pos]
137 143 if d == b'\\': # skip over escaped characters
138 144 pos += 2
139 145 continue
140 146 if d == c:
141 147 yield (b'string', program[s:pos], s)
142 148 break
143 149 pos += 1
144 150 else:
145 151 raise error.ParseError(_(b"unterminated string"), s)
146 152 elif c.isdigit():
147 153 s = pos
148 154 while pos < end:
149 155 d = program[pos]
150 156 if not d.isdigit():
151 157 break
152 158 pos += 1
153 159 yield (b'integer', program[s:pos], s)
154 160 pos -= 1
155 161 elif (
156 162 c == b'\\'
157 163 and program[pos : pos + 2] in (br"\'", br'\"')
158 164 or c == b'r'
159 165 and program[pos : pos + 3] in (br"r\'", br'r\"')
160 166 ):
161 167 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
162 168 # where some of nested templates were preprocessed as strings and
163 169 # then compiled. therefore, \"...\" was allowed. (issue4733)
164 170 #
165 171 # processing flow of _evalifliteral() at 5ab28a2e9962:
166 172 # outer template string -> stringify() -> compiletemplate()
167 173 # ------------------------ ------------ ------------------
168 174 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
169 175 # ~~~~~~~~
170 176 # escaped quoted string
171 177 if c == b'r':
172 178 pos += 1
173 179 token = b'string'
174 180 else:
175 181 token = b'template'
176 182 quote = program[pos : pos + 2]
177 183 s = pos = pos + 2
178 184 while pos < end: # find closing escaped quote
179 185 # pycompat.bytestr (and bytes) both have .startswith() that
180 186 # takes an optional start and an optional end, but pytype thinks
181 187 # it only takes 2 args.
182 188
183 189 # pytype: disable=wrong-arg-count
184 190 if program.startswith(b'\\\\\\', pos, end):
185 191 pos += 4 # skip over double escaped characters
186 192 continue
187 193 if program.startswith(quote, pos, end):
188 194 # pytype: enable=wrong-arg-count
189 195
190 196 # interpret as if it were a part of an outer string
191 197 data = parser.unescapestr(program[s:pos])
192 198 if token == b'template':
193 199 data = _parsetemplate(data, 0, len(data))[0]
194 200 yield (token, data, s)
195 201 pos += 1
196 202 break
197 203 pos += 1
198 204 else:
199 205 raise error.ParseError(_(b"unterminated string"), s)
200 206 elif c.isalnum() or c in b'_':
201 207 s = pos
202 208 pos += 1
203 209 while pos < end: # find end of symbol
204 210 d = program[pos]
205 211 if not (d.isalnum() or d == b"_"):
206 212 break
207 213 pos += 1
208 214 sym = program[s:pos]
209 215 yield (b'symbol', sym, s)
210 216 pos -= 1
211 217 elif c == term:
212 218 yield (b'end', None, pos)
213 219 return
214 220 else:
215 221 raise error.ParseError(_(b"syntax error"), pos)
216 222 pos += 1
217 223 if term:
218 224 raise error.ParseError(_(b"unterminated template expansion"), start)
219 225 yield (b'end', None, pos)
220 226
221 227
222 228 def _parsetemplate(tmpl, start, stop, quote=b''):
223 229 r"""
224 230 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
225 231 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
226 232 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
227 233 ([('string', 'foo'), ('symbol', 'bar')], 9)
228 234 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
229 235 ([('string', 'foo')], 4)
230 236 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
231 237 ([('string', 'foo"'), ('string', 'bar')], 9)
232 238 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
233 239 ([('string', 'foo\\')], 6)
234 240 """
235 241 parsed = []
236 242 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
237 243 if typ == b'string':
238 244 parsed.append((typ, val))
239 245 elif typ == b'template':
240 246 parsed.append(val)
241 247 elif typ == b'end':
242 248 return parsed, pos
243 249 else:
244 250 raise error.ProgrammingError(b'unexpected type: %s' % typ)
245 251 raise error.ProgrammingError(b'unterminated scanning of template')
246 252
247 253
248 254 def scantemplate(tmpl, raw=False):
249 255 r"""Scan (type, start, end) positions of outermost elements in template
250 256
251 257 If raw=True, a backslash is not taken as an escape character just like
252 258 r'' string in Python. Note that this is different from r'' literal in
253 259 template in that no template fragment can appear in r'', e.g. r'{foo}'
254 260 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
255 261 'foo'.
256 262
257 263 >>> list(scantemplate(b'foo{bar}"baz'))
258 264 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
259 265 >>> list(scantemplate(b'outer{"inner"}outer'))
260 266 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
261 267 >>> list(scantemplate(b'foo\\{escaped}'))
262 268 [('string', 0, 5), ('string', 5, 13)]
263 269 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
264 270 [('string', 0, 4), ('template', 4, 13)]
265 271 """
266 272 last = None
267 273 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
268 274 if last:
269 275 yield last + (pos,)
270 276 if typ == b'end':
271 277 return
272 278 else:
273 279 last = (typ, pos)
274 280 raise error.ProgrammingError(b'unterminated scanning of template')
275 281
276 282
277 283 def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
278 284 """Parse template string into chunks of strings and template expressions"""
279 285 sepchars = b'{' + quote
280 286 unescape = [parser.unescapestr, pycompat.identity][raw]
281 287 pos = start
282 288 p = parser.parser(elements)
283 289 try:
284 290 while pos < stop:
285 291 n = min(
286 292 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
287 293 key=lambda n: (n < 0, n),
288 294 )
289 295 if n < 0:
290 296 yield (b'string', unescape(tmpl[pos:stop]), pos)
291 297 pos = stop
292 298 break
293 299 c = tmpl[n : n + 1]
294 300 bs = 0 # count leading backslashes
295 301 if not raw:
296 302 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
297 303 if bs % 2 == 1:
298 304 # escaped (e.g. '\{', '\\\{', but not '\\{')
299 305 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
300 306 pos = n + 1
301 307 continue
302 308 if n > pos:
303 309 yield (b'string', unescape(tmpl[pos:n]), pos)
304 310 if c == quote:
305 311 yield (b'end', None, n + 1)
306 312 return
307 313
308 314 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
309 315
310 316 # pycompat.bytestr (and bytes) both have .startswith() that
311 317 # takes an optional start and an optional end, but pytype thinks
312 318 # it only takes 2 args.
313 319
314 320 # pytype: disable=wrong-arg-count
315 321 if not tmpl.startswith(b'}', pos):
316 322 # pytype: enable=wrong-arg-count
317 323 raise error.ParseError(_(b"invalid token"), pos)
318 324 yield (b'template', parseres, n)
319 325 pos += 1
320 326
321 327 if quote:
322 328 raise error.ParseError(_(b"unterminated string"), start)
323 329 except error.ParseError as inst:
324 330 _addparseerrorhint(inst, tmpl)
325 331 raise
326 332 yield (b'end', None, pos)
327 333
328 334
329 335 def _addparseerrorhint(inst, tmpl):
330 336 if inst.location is None:
331 337 return
332 338 loc = inst.location
333 339 # Offset the caret location by the number of newlines before the
334 340 # location of the error, since we will replace one-char newlines
335 341 # with the two-char literal r'\n'.
336 342 offset = tmpl[:loc].count(b'\n')
337 343 tmpl = tmpl.replace(b'\n', br'\n')
338 344 # We want the caret to point to the place in the template that
339 345 # failed to parse, but in a hint we get a open paren at the
340 346 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
341 347 # to line up the caret with the location of the error.
342 348 inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
343 349
344 350
345 351 def _unnesttemplatelist(tree):
346 352 """Expand list of templates to node tuple
347 353
348 354 >>> def f(tree):
349 355 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
350 356 >>> f((b'template', []))
351 357 (string '')
352 358 >>> f((b'template', [(b'string', b'foo')]))
353 359 (string 'foo')
354 360 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
355 361 (template
356 362 (string 'foo')
357 363 (symbol 'rev'))
358 364 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
359 365 (template
360 366 (symbol 'rev'))
361 367 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
362 368 (string 'foo')
363 369 """
364 370 if not isinstance(tree, tuple):
365 371 return tree
366 372 op = tree[0]
367 373 if op != b'template':
368 374 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
369 375
370 376 assert len(tree) == 2
371 377 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
372 378 if not xs:
373 379 return (b'string', b'') # empty template ""
374 380 elif len(xs) == 1 and xs[0][0] == b'string':
375 381 return xs[0] # fast path for string with no template fragment "x"
376 382 else:
377 383 return (op,) + xs
378 384
379 385
380 386 def parse(tmpl):
381 387 """Parse template string into tree"""
382 388 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
383 389 assert pos == len(tmpl), b'unquoted template should be consumed'
384 390 return _unnesttemplatelist((b'template', parsed))
385 391
386 392
387 393 def parseexpr(expr):
388 394 """Parse a template expression into tree
389 395
390 396 >>> parseexpr(b'"foo"')
391 397 ('string', 'foo')
392 398 >>> parseexpr(b'foo(bar)')
393 399 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
394 400 >>> from . import error
395 401 >>> from . import pycompat
396 402 >>> try:
397 403 ... parseexpr(b'foo(')
398 404 ... except error.ParseError as e:
399 405 ... pycompat.sysstr(e.message)
400 406 ... e.location
401 407 'not a prefix: end'
402 408 4
403 409 >>> try:
404 410 ... parseexpr(b'"foo" "bar"')
405 411 ... except error.ParseError as e:
406 412 ... pycompat.sysstr(e.message)
407 413 ... e.location
408 414 'invalid token'
409 415 7
410 416 """
411 417 try:
412 418 return _parseexpr(expr)
413 419 except error.ParseError as inst:
414 420 _addparseerrorhint(inst, expr)
415 421 raise
416 422
417 423
418 424 def _parseexpr(expr):
419 425 p = parser.parser(elements)
420 426 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
421 427 if pos != len(expr):
422 428 raise error.ParseError(_(b'invalid token'), pos)
423 429 return _unnesttemplatelist(tree)
424 430
425 431
426 432 def prettyformat(tree):
427 433 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
428 434
429 435
430 436 def compileexp(exp, context, curmethods):
431 437 """Compile parsed template tree to (func, data) pair"""
432 438 if not exp:
433 439 raise error.ParseError(_(b"missing argument"))
434 440 t = exp[0]
435 441 return curmethods[t](exp, context)
436 442
437 443
438 444 # template evaluation
439 445
440 446
441 447 def getsymbol(exp):
442 448 if exp[0] == b'symbol':
443 449 return exp[1]
444 450 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
445 451
446 452
447 453 def getlist(x):
448 454 if not x:
449 455 return []
450 456 if x[0] == b'list':
451 457 return getlist(x[1]) + [x[2]]
452 458 return [x]
453 459
454 460
455 461 def gettemplate(exp, context):
456 462 """Compile given template tree or load named template from map file;
457 463 returns (func, data) pair"""
458 464 if exp[0] in (b'template', b'string'):
459 465 return compileexp(exp, context, methods)
460 466 if exp[0] == b'symbol':
461 467 # unlike runsymbol(), here 'symbol' is always taken as template name
462 468 # even if it exists in mapping. this allows us to override mapping
463 469 # by web templates, e.g. 'changelogtag' is redefined in map file.
464 470 return context._load(exp[1])
465 471 raise error.ParseError(_(b"expected template specifier"))
466 472
467 473
468 474 def _runrecursivesymbol(context, mapping, key):
469 475 raise error.InputError(_(b"recursive reference '%s' in template") % key)
470 476
471 477
472 478 def buildtemplate(exp, context):
473 479 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
474 480 return (templateutil.runtemplate, ctmpl)
475 481
476 482
477 483 def buildfilter(exp, context):
478 484 n = getsymbol(exp[2])
479 485 if n in context._filters:
480 486 filt = context._filters[n]
481 487 arg = compileexp(exp[1], context, methods)
482 488 return (templateutil.runfilter, (arg, filt))
483 489 if n in context._funcs:
484 490 f = context._funcs[n]
485 491 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
486 492 return (f, args)
487 493 raise error.ParseError(_(b"unknown function '%s'") % n)
488 494
489 495
490 496 def buildmap(exp, context):
491 497 darg = compileexp(exp[1], context, methods)
492 498 targ = gettemplate(exp[2], context)
493 499 return (templateutil.runmap, (darg, targ))
494 500
495 501
496 502 def buildmember(exp, context):
497 503 darg = compileexp(exp[1], context, methods)
498 504 memb = getsymbol(exp[2])
499 505 return (templateutil.runmember, (darg, memb))
500 506
501 507
502 508 def buildnegate(exp, context):
503 509 arg = compileexp(exp[1], context, exprmethods)
504 510 return (templateutil.runnegate, arg)
505 511
506 512
507 513 def buildarithmetic(exp, context, func):
508 514 left = compileexp(exp[1], context, exprmethods)
509 515 right = compileexp(exp[2], context, exprmethods)
510 516 return (templateutil.runarithmetic, (func, left, right))
511 517
512 518
513 519 def buildfunc(exp, context):
514 520 n = getsymbol(exp[1])
515 521 if n in context._funcs:
516 522 f = context._funcs[n]
517 523 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
518 524 return (f, args)
519 525 if n in context._filters:
520 526 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
521 527 if len(args) != 1:
522 528 raise error.ParseError(_(b"filter %s expects one argument") % n)
523 529 f = context._filters[n]
524 530 return (templateutil.runfilter, (args[0], f))
525 531 raise error.ParseError(_(b"unknown function '%s'") % n)
526 532
527 533
528 534 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
529 535 """Compile parsed tree of function arguments into list or dict of
530 536 (func, data) pairs
531 537
532 538 >>> context = engine(lambda t: (templateutil.runsymbol, t))
533 539 >>> def fargs(expr, argspec):
534 540 ... x = _parseexpr(expr)
535 541 ... n = getsymbol(x[1])
536 542 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
537 543 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
538 544 ['l', 'k']
539 545 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
540 546 >>> list(args.keys()), list(args[b'opts'].keys())
541 547 (['opts'], ['opts', 'k'])
542 548 """
543 549
544 550 def compiledict(xs):
545 551 return util.sortdict(
546 552 (k, compileexp(x, context, curmethods)) for k, x in xs.items()
547 553 )
548 554
549 555 def compilelist(xs):
550 556 return [compileexp(x, context, curmethods) for x in xs]
551 557
552 558 if not argspec:
553 559 # filter or function with no argspec: return list of positional args
554 560 return compilelist(getlist(exp))
555 561
556 562 # function with argspec: return dict of named args
557 563 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
558 564 treeargs = parser.buildargsdict(
559 565 getlist(exp),
560 566 funcname,
561 567 argspec,
562 568 keyvaluenode=b'keyvalue',
563 569 keynode=b'symbol',
564 570 )
565 571 compargs = util.sortdict()
566 572 if varkey:
567 573 compargs[varkey] = compilelist(treeargs.pop(varkey))
568 574 if optkey:
569 575 compargs[optkey] = compiledict(treeargs.pop(optkey))
570 576 compargs.update(compiledict(treeargs))
571 577 return compargs
572 578
573 579
574 580 def buildkeyvaluepair(exp, content):
575 581 raise error.ParseError(_(b"can't use a key-value pair in this context"))
576 582
577 583
578 584 def buildlist(exp, context):
579 585 raise error.ParseError(
580 586 _(b"can't use a list in this context"),
581 587 hint=_(b'check place of comma and parens'),
582 588 )
583 589
584 590
585 591 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
586 592 exprmethods = {
587 593 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
588 594 b"string": lambda e, c: (templateutil.runstring, e[1]),
589 595 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
590 596 b"template": buildtemplate,
591 597 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
592 598 b".": buildmember,
593 599 b"|": buildfilter,
594 600 b"%": buildmap,
595 601 b"func": buildfunc,
596 602 b"keyvalue": buildkeyvaluepair,
597 603 b"list": buildlist,
598 604 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
599 605 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
600 606 b"negate": buildnegate,
601 607 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
602 608 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
603 609 }
604 610
605 611 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
606 612 methods = exprmethods.copy()
607 613 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
608 614
609 615
610 616 class _aliasrules(parser.basealiasrules):
611 617 """Parsing and expansion rule set of template aliases"""
612 618
613 619 _section = _(b'template alias')
614 620 _parse = staticmethod(_parseexpr)
615 621
616 622 @staticmethod
617 623 def _trygetfunc(tree):
618 624 """Return (name, args) if tree is func(...) or ...|filter; otherwise
619 625 None"""
620 626 if tree[0] == b'func' and tree[1][0] == b'symbol':
621 627 return tree[1][1], getlist(tree[2])
622 628 if tree[0] == b'|' and tree[2][0] == b'symbol':
623 629 return tree[2][1], [tree[1]]
624 630
625 631
626 632 def expandaliases(tree, aliases):
627 633 """Return new tree of aliases are expanded"""
628 634 aliasmap = _aliasrules.buildmap(aliases)
629 635 return _aliasrules.expand(aliasmap, tree)
630 636
631 637
632 638 # template engine
633 639
634 640
635 641 def unquotestring(s):
636 642 '''unwrap quotes if any; otherwise returns unmodified string'''
637 643 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
638 644 return s
639 645 return s[1:-1]
640 646
641 647
642 648 class resourcemapper: # pytype: disable=ignored-metaclass
643 649 """Mapper of internal template resources"""
644 650
645 651 __metaclass__ = abc.ABCMeta
646 652
647 653 @abc.abstractmethod
648 654 def availablekeys(self, mapping):
649 655 """Return a set of available resource keys based on the given mapping"""
650 656
651 657 @abc.abstractmethod
652 658 def knownkeys(self):
653 659 """Return a set of supported resource keys"""
654 660
655 661 @abc.abstractmethod
656 662 def lookup(self, mapping, key):
657 663 """Return a resource for the key if available; otherwise None"""
658 664
659 665 @abc.abstractmethod
660 666 def populatemap(self, context, origmapping, newmapping):
661 667 """Return a dict of additional mapping items which should be paired
662 668 with the given new mapping"""
663 669
664 670
665 671 class nullresourcemapper(resourcemapper):
666 672 def availablekeys(self, mapping):
667 673 return set()
668 674
669 675 def knownkeys(self):
670 676 return set()
671 677
672 678 def lookup(self, mapping, key):
673 679 return None
674 680
675 681 def populatemap(self, context, origmapping, newmapping):
676 682 return {}
677 683
678 684
679 685 class engine:
680 686 """template expansion engine.
681 687
682 688 template expansion works like this. a map file contains key=value
683 689 pairs. if value is quoted, it is treated as string. otherwise, it
684 690 is treated as name of template file.
685 691
686 692 templater is asked to expand a key in map. it looks up key, and
687 693 looks for strings like this: {foo}. it expands {foo} by looking up
688 694 foo in map, and substituting it. expansion is recursive: it stops
689 695 when there is no more {foo} to replace.
690 696
691 697 expansion also allows formatting and filtering.
692 698
693 699 format uses key to expand each item in list. syntax is
694 700 {key%format}.
695 701
696 702 filter uses function to transform value. syntax is
697 703 {key|filter1|filter2|...}."""
698 704
699 705 def __init__(self, loader, filters=None, defaults=None, resources=None):
700 706 self._loader = loader
701 707 if filters is None:
702 708 filters = {}
703 709 self._filters = filters
704 710 self._funcs = templatefuncs.funcs # make this a parameter if needed
705 711 if defaults is None:
706 712 defaults = {}
707 713 if resources is None:
708 714 resources = nullresourcemapper()
709 715 self._defaults = defaults
710 716 self._resources = resources
711 717 self._cache = {} # key: (func, data)
712 718 self._tmplcache = {} # literal template: (func, data)
713 719
714 720 def overlaymap(self, origmapping, newmapping):
715 721 """Create combined mapping from the original mapping and partial
716 722 mapping to override the original"""
717 723 # do not copy symbols which overrides the defaults depending on
718 724 # new resources, so the defaults will be re-evaluated (issue5612)
719 725 knownres = self._resources.knownkeys()
720 726 newres = self._resources.availablekeys(newmapping)
721 727 mapping = {
722 728 k: v
723 729 for k, v in origmapping.items()
724 730 if (
725 731 k in knownres # not a symbol per self.symbol()
726 732 or newres.isdisjoint(self._defaultrequires(k))
727 733 )
728 734 }
729 735 mapping.update(newmapping)
730 736 mapping.update(
731 737 self._resources.populatemap(self, origmapping, newmapping)
732 738 )
733 739 return mapping
734 740
735 741 def _defaultrequires(self, key):
736 742 """Resource keys required by the specified default symbol function"""
737 743 v = self._defaults.get(key)
738 744 if v is None or not callable(v):
739 745 return ()
740 746 return getattr(v, '_requires', ())
741 747
742 748 def symbol(self, mapping, key):
743 749 """Resolve symbol to value or function; None if nothing found"""
744 750 v = None
745 751 if key not in self._resources.knownkeys():
746 752 v = mapping.get(key)
747 753 if v is None:
748 754 v = self._defaults.get(key)
749 755 return v
750 756
751 757 def availableresourcekeys(self, mapping):
752 758 """Return a set of available resource keys based on the given mapping"""
753 759 return self._resources.availablekeys(mapping)
754 760
755 761 def knownresourcekeys(self):
756 762 """Return a set of supported resource keys"""
757 763 return self._resources.knownkeys()
758 764
759 765 def resource(self, mapping, key):
760 766 """Return internal data (e.g. cache) used for keyword/function
761 767 evaluation"""
762 768 v = self._resources.lookup(mapping, key)
763 769 if v is None:
764 770 raise templateutil.ResourceUnavailable(
765 771 _(b'template resource not available: %s') % key
766 772 )
767 773 return v
768 774
769 775 def _load(self, t):
770 776 '''load, parse, and cache a template'''
771 777 if t not in self._cache:
772 778 x = self._loader(t)
773 779 # put poison to cut recursion while compiling 't'
774 780 self._cache[t] = (_runrecursivesymbol, t)
775 781 try:
776 782 self._cache[t] = compileexp(x, self, methods)
777 783 except: # re-raises
778 784 del self._cache[t]
779 785 raise
780 786 return self._cache[t]
781 787
782 788 def _parse(self, tmpl):
783 789 """Parse and cache a literal template"""
784 790 if tmpl not in self._tmplcache:
785 791 x = parse(tmpl)
786 792 self._tmplcache[tmpl] = compileexp(x, self, methods)
787 793 return self._tmplcache[tmpl]
788 794
789 795 def preload(self, t):
790 796 """Load, parse, and cache the specified template if available"""
791 797 try:
792 798 self._load(t)
793 799 return True
794 800 except templateutil.TemplateNotFound:
795 801 return False
796 802
797 803 def process(self, t, mapping):
798 804 """Perform expansion. t is name of map element to expand.
799 805 mapping contains added elements for use during expansion. Is a
800 806 generator."""
801 807 func, data = self._load(t)
802 808 return self._expand(func, data, mapping)
803 809
804 810 def expand(self, tmpl, mapping):
805 811 """Perform expansion over a literal template
806 812
807 813 No user aliases will be expanded since this is supposed to be called
808 814 with an internal template string.
809 815 """
810 816 func, data = self._parse(tmpl)
811 817 return self._expand(func, data, mapping)
812 818
813 819 def _expand(self, func, data, mapping):
814 820 # populate additional items only if they don't exist in the given
815 821 # mapping. this is slightly different from overlaymap() because the
816 822 # initial 'revcache' may contain pre-computed items.
817 823 extramapping = self._resources.populatemap(self, {}, mapping)
818 824 if extramapping:
819 825 extramapping.update(mapping)
820 826 mapping = extramapping
821 827 return templateutil.flatten(self, mapping, func(self, mapping, data))
822 828
823 829
824 830 def stylelist():
825 831 path = templatedir()
826 832 if not path:
827 833 return _(b'no templates found, try `hg debuginstall` for more info')
828 834 dirlist = os.listdir(path)
829 835 stylelist = []
830 836 for file in dirlist:
831 837 split = file.split(b".")
832 838 if split[-1] in (b'orig', b'rej'):
833 839 continue
834 840 if split[0] == b"map-cmdline":
835 841 stylelist.append(split[1])
836 842 return b", ".join(sorted(stylelist))
837 843
838 844
839 845 def _open_mapfile(mapfile):
840 846 if os.path.exists(mapfile):
841 847 return util.posixfile(mapfile, b'rb')
842 848 raise error.Abort(
843 849 _(b"style '%s' not found") % mapfile,
844 850 hint=_(b"available styles: %s") % stylelist(),
845 851 )
846 852
847 853
848 854 def _readmapfile(fp, mapfile):
849 855 """Load template elements from the given map file"""
850 856 if pycompat.iswindows:
851 857 # quick hack to make sure we can process '/' in the code dealing with
852 858 # ressource. Ideally we would make sure we use `/` instead of `ossep`
853 859 # in the templater code, but that seems a bigger and less certain
854 860 # change that we better left for the default branch.
855 861 name_paths = mapfile.split(pycompat.ossep)
856 862 mapfile = b'/'.join(name_paths)
857 863 base = os.path.dirname(mapfile)
858 864 conf = config.config()
859 865
860 866 def include(rel, remap, sections):
861 867 subresource = None
862 868 if base:
863 869 abs = os.path.normpath(os.path.join(base, rel))
864 870 if os.path.isfile(abs):
865 871 subresource = util.posixfile(abs, b'rb')
866 872 if not subresource:
867 873 if pycompat.ossep not in rel:
868 874 abs = rel
869 875 try:
870 876 subresource = resourceutil.open_resource(
871 877 b'mercurial.templates', rel
872 878 )
873 879 except FileNotFoundError:
874 880 subresource = None
875 881 else:
876 882 dir = templatedir()
877 883 if dir:
878 884 abs = os.path.normpath(os.path.join(dir, rel))
879 885 if os.path.isfile(abs):
880 886 subresource = util.posixfile(abs, b'rb')
881 887 if subresource:
882 888 data = subresource.read()
883 889 conf.parse(
884 890 abs,
885 891 data,
886 892 sections=sections,
887 893 remap=remap,
888 894 include=include,
889 895 )
890 896
891 897 data = fp.read()
892 898 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
893 899
894 900 cache = {}
895 901 tmap = {}
896 902 aliases = []
897 903
898 904 val = conf.get(b'templates', b'__base__')
899 905 if val and val[0] not in b"'\"":
900 906 # treat as a pointer to a base class for this style
901 907 path = os.path.normpath(os.path.join(base, val))
902 908
903 909 # fallback check in template paths
904 910 if not os.path.exists(path):
905 911 dir = templatedir()
906 912 if dir is not None:
907 913 p2 = os.path.normpath(os.path.join(dir, val))
908 914 if os.path.isfile(p2):
909 915 path = p2
910 916 else:
911 917 p3 = os.path.normpath(os.path.join(p2, b"map"))
912 918 if os.path.isfile(p3):
913 919 path = p3
914 920
915 921 fp = _open_mapfile(path)
916 922 cache, tmap, aliases = _readmapfile(fp, path)
917 923
918 924 for key, val in conf.items(b'templates'):
919 925 if not val:
920 926 raise error.ParseError(
921 927 _(b'missing value'), conf.source(b'templates', key)
922 928 )
923 929 if val[0] in b"'\"":
924 930 if val[0] != val[-1]:
925 931 raise error.ParseError(
926 932 _(b'unmatched quotes'), conf.source(b'templates', key)
927 933 )
928 934 cache[key] = unquotestring(val)
929 935 elif key != b'__base__':
930 936 tmap[key] = os.path.join(base, val)
931 937 aliases.extend(conf.items(b'templatealias'))
932 938 return cache, tmap, aliases
933 939
934 940
935 941 class loader:
936 942 """Load template fragments optionally from a map file"""
937 943
938 944 def __init__(self, cache, aliases):
939 945 if cache is None:
940 946 cache = {}
941 947 self.cache = cache.copy()
942 948 self._map = {}
943 949 self._aliasmap = _aliasrules.buildmap(aliases)
944 950
945 951 def __contains__(self, key):
946 952 return key in self.cache or key in self._map
947 953
948 954 def load(self, t):
949 955 """Get parsed tree for the given template name. Use a local cache."""
950 956 if t not in self.cache:
951 957 try:
952 958 mapfile, fp = open_template(self._map[t])
953 959 self.cache[t] = fp.read()
954 960 except KeyError as inst:
955 961 raise templateutil.TemplateNotFound(
956 962 _(b'"%s" not in template map') % inst.args[0]
957 963 )
958 964 except IOError as inst:
959 965 reason = _(b'template file %s: %s') % (
960 966 self._map[t],
961 967 stringutil.forcebytestr(inst.args[1]),
962 968 )
963 969 raise IOError(inst.args[0], encoding.strfromlocal(reason))
964 970 return self._parse(self.cache[t])
965 971
966 972 def _parse(self, tmpl):
967 973 x = parse(tmpl)
968 974 if self._aliasmap:
969 975 x = _aliasrules.expand(self._aliasmap, x)
970 976 return x
971 977
972 978 def _findsymbolsused(self, tree, syms):
973 979 if not tree:
974 980 return
975 981 op = tree[0]
976 982 if op == b'symbol':
977 983 s = tree[1]
978 984 if s in syms[0]:
979 985 return # avoid recursion: s -> cache[s] -> s
980 986 syms[0].add(s)
981 987 if s in self.cache or s in self._map:
982 988 # s may be a reference for named template
983 989 self._findsymbolsused(self.load(s), syms)
984 990 return
985 991 if op in {b'integer', b'string'}:
986 992 return
987 993 # '{arg|func}' == '{func(arg)}'
988 994 if op == b'|':
989 995 syms[1].add(getsymbol(tree[2]))
990 996 self._findsymbolsused(tree[1], syms)
991 997 return
992 998 if op == b'func':
993 999 syms[1].add(getsymbol(tree[1]))
994 1000 self._findsymbolsused(tree[2], syms)
995 1001 return
996 1002 for x in tree[1:]:
997 1003 self._findsymbolsused(x, syms)
998 1004
999 1005 def symbolsused(self, t):
1000 1006 """Look up (keywords, filters/functions) referenced from the name
1001 1007 template 't'
1002 1008
1003 1009 This may load additional templates from the map file.
1004 1010 """
1005 1011 syms = (set(), set())
1006 1012 self._findsymbolsused(self.load(t), syms)
1007 1013 return syms
1008 1014
1009 1015
1010 1016 class templater:
1011 1017 def __init__(
1012 1018 self,
1013 1019 filters=None,
1014 1020 defaults=None,
1015 1021 resources=None,
1016 1022 cache=None,
1017 1023 aliases=(),
1018 1024 minchunk=1024,
1019 1025 maxchunk=65536,
1020 1026 ):
1021 1027 """Create template engine optionally with preloaded template fragments
1022 1028
1023 1029 - ``filters``: a dict of functions to transform a value into another.
1024 1030 - ``defaults``: a dict of symbol values/functions; may be overridden
1025 1031 by a ``mapping`` dict.
1026 1032 - ``resources``: a resourcemapper object to look up internal data
1027 1033 (e.g. cache), inaccessible from user template.
1028 1034 - ``cache``: a dict of preloaded template fragments.
1029 1035 - ``aliases``: a list of alias (name, replacement) pairs.
1030 1036
1031 1037 self.cache may be updated later to register additional template
1032 1038 fragments.
1033 1039 """
1034 1040 allfilters = templatefilters.filters.copy()
1035 1041 if filters:
1036 1042 allfilters.update(filters)
1037 1043 self._loader = loader(cache, aliases)
1038 1044 self._proc = engine(self._loader.load, allfilters, defaults, resources)
1039 1045 self._minchunk, self._maxchunk = minchunk, maxchunk
1040 1046
1041 1047 @classmethod
1042 1048 def frommapfile(
1043 1049 cls,
1044 1050 mapfile,
1045 1051 fp=None,
1046 1052 filters=None,
1047 1053 defaults=None,
1048 1054 resources=None,
1049 1055 cache=None,
1050 1056 minchunk=1024,
1051 1057 maxchunk=65536,
1052 1058 ):
1053 1059 """Create templater from the specified map file"""
1054 1060 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1055 1061 if not fp:
1056 1062 fp = _open_mapfile(mapfile)
1057 1063 cache, tmap, aliases = _readmapfile(fp, mapfile)
1058 1064 t._loader.cache.update(cache)
1059 1065 t._loader._map = tmap
1060 1066 t._loader._aliasmap = _aliasrules.buildmap(aliases)
1061 1067 return t
1062 1068
1063 1069 def __contains__(self, key):
1064 1070 return key in self._loader
1065 1071
1066 1072 @property
1067 1073 def cache(self):
1068 1074 return self._loader.cache
1069 1075
1070 1076 # for highlight extension to insert one-time 'colorize' filter
1071 1077 @property
1072 1078 def _filters(self):
1073 1079 return self._proc._filters
1074 1080
1075 1081 @property
1076 1082 def defaults(self):
1077 1083 return self._proc._defaults
1078 1084
1079 1085 def load(self, t):
1080 1086 """Get parsed tree for the given template name. Use a local cache."""
1081 1087 return self._loader.load(t)
1082 1088
1083 1089 def symbolsuseddefault(self):
1084 1090 """Look up (keywords, filters/functions) referenced from the default
1085 1091 unnamed template
1086 1092
1087 1093 This may load additional templates from the map file.
1088 1094 """
1089 1095 return self.symbolsused(b'')
1090 1096
1091 1097 def symbolsused(self, t):
1092 1098 """Look up (keywords, filters/functions) referenced from the name
1093 1099 template 't'
1094 1100
1095 1101 This may load additional templates from the map file.
1096 1102 """
1097 1103 return self._loader.symbolsused(t)
1098 1104
1099 1105 def renderdefault(self, mapping):
1100 1106 """Render the default unnamed template and return result as string"""
1101 1107 return self.render(b'', mapping)
1102 1108
1103 1109 def render(self, t, mapping):
1104 1110 """Render the specified named template and return result as string"""
1105 1111 return b''.join(self.generate(t, mapping))
1106 1112
1107 1113 def generate(self, t, mapping):
1108 1114 """Return a generator that renders the specified named template and
1109 1115 yields chunks"""
1110 1116 stream = self._proc.process(t, mapping)
1111 1117 if self._minchunk:
1112 1118 stream = util.increasingchunks(
1113 1119 stream, min=self._minchunk, max=self._maxchunk
1114 1120 )
1115 1121 return stream
1116 1122
1117 1123
1118 1124 def templatedir():
1119 1125 '''return the directory used for template files, or None.'''
1120 1126 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1121 1127 return path if os.path.isdir(path) else None
1122 1128
1123 1129
1124 def open_template(name, templatepath=None):
1130 def open_template(
1131 name: bytes, templatepath: Optional[bytes] = None
1132 ) -> Tuple[bytes, BinaryIO]:
1125 1133 """returns a file-like object for the given template, and its full path
1126 1134
1127 1135 If the name is a relative path and we're in a frozen binary, the template
1128 1136 will be read from the mercurial.templates package instead. The returned path
1129 1137 will then be the relative path.
1130 1138 """
1131 1139 # Does the name point directly to a map file?
1132 1140 if os.path.isfile(name) or os.path.isabs(name):
1133 1141 return name, open(name, mode='rb')
1134 1142
1135 1143 # Does the name point to a template in the provided templatepath, or
1136 1144 # in mercurial/templates/ if no path was provided?
1137 1145 if templatepath is None:
1138 1146 templatepath = templatedir()
1139 1147 if templatepath is not None:
1140 1148 f = os.path.join(templatepath, name)
1141 1149 return f, open(f, mode='rb')
1142 1150
1143 1151 # Otherwise try to read it using the resources API
1144 1152 if pycompat.iswindows:
1145 1153 # quick hack to make sure we can process '/' in the code dealing with
1146 1154 # ressource. Ideally we would make sure we use `/` instead of `ossep`
1147 1155 # in the templater code, but that seems a bigger and less certain
1148 1156 # change that we better left for the default branch.
1149 1157 name_paths = name.split(pycompat.ossep)
1150 1158 name = b'/'.join(name_paths)
1151 1159 name_parts = name.split(b'/')
1152 1160 package_name = b'.'.join([b'mercurial', b'templates'] + name_parts[:-1])
1153 1161 return (
1154 1162 name,
1155 1163 resourceutil.open_resource(package_name, name_parts[-1]),
1156 1164 )
1157 1165
1158 1166
1159 def try_open_template(name, templatepath=None):
1167 def try_open_template(
1168 name: bytes, templatepath: Optional[bytes] = None
1169 ) -> Tuple[Optional[bytes], Optional[BinaryIO]]:
1160 1170 try:
1161 1171 return open_template(name, templatepath)
1162 1172 except (EnvironmentError, ImportError):
1163 1173 return None, None
General Comments 0
You need to be logged in to leave comments. Login now