##// END OF EJS Templates
python3: move from using func_name to __name__...
Augie Fackler -
r34694:56bb07a0 default
parent child Browse files
Show More
@@ -1,96 +1,96 b''
1 1 # peer.py - repository base classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 from . import (
12 12 error,
13 13 util,
14 14 )
15 15
16 16 # abstract batching support
17 17
18 18 class future(object):
19 19 '''placeholder for a value to be set later'''
20 20 def set(self, value):
21 21 if util.safehasattr(self, 'value'):
22 22 raise error.RepoError("future is already set")
23 23 self.value = value
24 24
25 25 class batcher(object):
26 26 '''base class for batches of commands submittable in a single request
27 27
28 28 All methods invoked on instances of this class are simply queued and
29 29 return a a future for the result. Once you call submit(), all the queued
30 30 calls are performed and the results set in their respective futures.
31 31 '''
32 32 def __init__(self):
33 33 self.calls = []
34 34 def __getattr__(self, name):
35 35 def call(*args, **opts):
36 36 resref = future()
37 37 self.calls.append((name, args, opts, resref,))
38 38 return resref
39 39 return call
40 40 def submit(self):
41 41 raise NotImplementedError()
42 42
43 43 class iterbatcher(batcher):
44 44
45 45 def submit(self):
46 46 raise NotImplementedError()
47 47
48 48 def results(self):
49 49 raise NotImplementedError()
50 50
51 51 class localiterbatcher(iterbatcher):
52 52 def __init__(self, local):
53 53 super(iterbatcher, self).__init__()
54 54 self.local = local
55 55
56 56 def submit(self):
57 57 # submit for a local iter batcher is a noop
58 58 pass
59 59
60 60 def results(self):
61 61 for name, args, opts, resref in self.calls:
62 62 resref.set(getattr(self.local, name)(*args, **opts))
63 63 yield resref.value
64 64
65 65 def batchable(f):
66 66 '''annotation for batchable methods
67 67
68 68 Such methods must implement a coroutine as follows:
69 69
70 70 @batchable
71 71 def sample(self, one, two=None):
72 72 # Build list of encoded arguments suitable for your wire protocol:
73 73 encargs = [('one', encode(one),), ('two', encode(two),)]
74 74 # Create future for injection of encoded result:
75 75 encresref = future()
76 76 # Return encoded arguments and future:
77 77 yield encargs, encresref
78 78 # Assuming the future to be filled with the result from the batched
79 79 # request now. Decode it:
80 80 yield decode(encresref.value)
81 81
82 82 The decorator returns a function which wraps this coroutine as a plain
83 83 method, but adds the original method as an attribute called "batchable",
84 84 which is used by remotebatch to split the call into separate encoding and
85 85 decoding phases.
86 86 '''
87 87 def plain(*args, **opts):
88 88 batchable = f(*args, **opts)
89 89 encargsorres, encresref = next(batchable)
90 90 if not encresref:
91 91 return encargsorres # a local result in this case
92 92 self = args[0]
93 encresref.set(self._submitone(f.func_name, encargsorres))
93 encresref.set(self._submitone(f.__name__, encargsorres))
94 94 return next(batchable)
95 95 setattr(plain, 'batchable', f)
96 96 return plain
@@ -1,1518 +1,1518 b''
1 1 # templater.py - template expansion for output
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, print_function
9 9
10 10 import os
11 11 import re
12 12 import types
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 color,
17 17 config,
18 18 encoding,
19 19 error,
20 20 minirst,
21 21 obsutil,
22 22 parser,
23 23 pycompat,
24 24 registrar,
25 25 revset as revsetmod,
26 26 revsetlang,
27 27 scmutil,
28 28 templatefilters,
29 29 templatekw,
30 30 util,
31 31 )
32 32
33 33 # template parsing
34 34
35 35 elements = {
36 36 # token-type: binding-strength, primary, prefix, infix, suffix
37 37 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
38 38 ".": (18, None, None, (".", 18), None),
39 39 "%": (15, None, None, ("%", 15), None),
40 40 "|": (15, None, None, ("|", 15), None),
41 41 "*": (5, None, None, ("*", 5), None),
42 42 "/": (5, None, None, ("/", 5), None),
43 43 "+": (4, None, None, ("+", 4), None),
44 44 "-": (4, None, ("negate", 19), ("-", 4), None),
45 45 "=": (3, None, None, ("keyvalue", 3), None),
46 46 ",": (2, None, None, ("list", 2), None),
47 47 ")": (0, None, None, None, None),
48 48 "integer": (0, "integer", None, None, None),
49 49 "symbol": (0, "symbol", None, None, None),
50 50 "string": (0, "string", None, None, None),
51 51 "template": (0, "template", None, None, None),
52 52 "end": (0, None, None, None, None),
53 53 }
54 54
55 55 def tokenize(program, start, end, term=None):
56 56 """Parse a template expression into a stream of tokens, which must end
57 57 with term if specified"""
58 58 pos = start
59 59 program = pycompat.bytestr(program)
60 60 while pos < end:
61 61 c = program[pos]
62 62 if c.isspace(): # skip inter-token whitespace
63 63 pass
64 64 elif c in "(=,).%|+-*/": # handle simple operators
65 65 yield (c, None, pos)
66 66 elif c in '"\'': # handle quoted templates
67 67 s = pos + 1
68 68 data, pos = _parsetemplate(program, s, end, c)
69 69 yield ('template', data, s)
70 70 pos -= 1
71 71 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
72 72 # handle quoted strings
73 73 c = program[pos + 1]
74 74 s = pos = pos + 2
75 75 while pos < end: # find closing quote
76 76 d = program[pos]
77 77 if d == '\\': # skip over escaped characters
78 78 pos += 2
79 79 continue
80 80 if d == c:
81 81 yield ('string', program[s:pos], s)
82 82 break
83 83 pos += 1
84 84 else:
85 85 raise error.ParseError(_("unterminated string"), s)
86 86 elif c.isdigit():
87 87 s = pos
88 88 while pos < end:
89 89 d = program[pos]
90 90 if not d.isdigit():
91 91 break
92 92 pos += 1
93 93 yield ('integer', program[s:pos], s)
94 94 pos -= 1
95 95 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
96 96 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
97 97 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
98 98 # where some of nested templates were preprocessed as strings and
99 99 # then compiled. therefore, \"...\" was allowed. (issue4733)
100 100 #
101 101 # processing flow of _evalifliteral() at 5ab28a2e9962:
102 102 # outer template string -> stringify() -> compiletemplate()
103 103 # ------------------------ ------------ ------------------
104 104 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
105 105 # ~~~~~~~~
106 106 # escaped quoted string
107 107 if c == 'r':
108 108 pos += 1
109 109 token = 'string'
110 110 else:
111 111 token = 'template'
112 112 quote = program[pos:pos + 2]
113 113 s = pos = pos + 2
114 114 while pos < end: # find closing escaped quote
115 115 if program.startswith('\\\\\\', pos, end):
116 116 pos += 4 # skip over double escaped characters
117 117 continue
118 118 if program.startswith(quote, pos, end):
119 119 # interpret as if it were a part of an outer string
120 120 data = parser.unescapestr(program[s:pos])
121 121 if token == 'template':
122 122 data = _parsetemplate(data, 0, len(data))[0]
123 123 yield (token, data, s)
124 124 pos += 1
125 125 break
126 126 pos += 1
127 127 else:
128 128 raise error.ParseError(_("unterminated string"), s)
129 129 elif c.isalnum() or c in '_':
130 130 s = pos
131 131 pos += 1
132 132 while pos < end: # find end of symbol
133 133 d = program[pos]
134 134 if not (d.isalnum() or d == "_"):
135 135 break
136 136 pos += 1
137 137 sym = program[s:pos]
138 138 yield ('symbol', sym, s)
139 139 pos -= 1
140 140 elif c == term:
141 141 yield ('end', None, pos + 1)
142 142 return
143 143 else:
144 144 raise error.ParseError(_("syntax error"), pos)
145 145 pos += 1
146 146 if term:
147 147 raise error.ParseError(_("unterminated template expansion"), start)
148 148 yield ('end', None, pos)
149 149
150 150 def _parsetemplate(tmpl, start, stop, quote=''):
151 151 r"""
152 152 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
153 153 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
154 154 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
155 155 ([('string', 'foo'), ('symbol', 'bar')], 9)
156 156 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
157 157 ([('string', 'foo')], 4)
158 158 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
159 159 ([('string', 'foo"'), ('string', 'bar')], 9)
160 160 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
161 161 ([('string', 'foo\\')], 6)
162 162 """
163 163 parsed = []
164 164 sepchars = '{' + quote
165 165 pos = start
166 166 p = parser.parser(elements)
167 167 while pos < stop:
168 168 n = min((tmpl.find(c, pos, stop) for c in sepchars),
169 169 key=lambda n: (n < 0, n))
170 170 if n < 0:
171 171 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
172 172 pos = stop
173 173 break
174 174 c = tmpl[n:n + 1]
175 175 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
176 176 if bs % 2 == 1:
177 177 # escaped (e.g. '\{', '\\\{', but not '\\{')
178 178 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
179 179 pos = n + 1
180 180 continue
181 181 if n > pos:
182 182 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
183 183 if c == quote:
184 184 return parsed, n + 1
185 185
186 186 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
187 187 parsed.append(parseres)
188 188
189 189 if quote:
190 190 raise error.ParseError(_("unterminated string"), start)
191 191 return parsed, pos
192 192
193 193 def _unnesttemplatelist(tree):
194 194 """Expand list of templates to node tuple
195 195
196 196 >>> def f(tree):
197 197 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
198 198 >>> f((b'template', []))
199 199 (string '')
200 200 >>> f((b'template', [(b'string', b'foo')]))
201 201 (string 'foo')
202 202 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
203 203 (template
204 204 (string 'foo')
205 205 (symbol 'rev'))
206 206 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
207 207 (template
208 208 (symbol 'rev'))
209 209 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
210 210 (string 'foo')
211 211 """
212 212 if not isinstance(tree, tuple):
213 213 return tree
214 214 op = tree[0]
215 215 if op != 'template':
216 216 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
217 217
218 218 assert len(tree) == 2
219 219 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
220 220 if not xs:
221 221 return ('string', '') # empty template ""
222 222 elif len(xs) == 1 and xs[0][0] == 'string':
223 223 return xs[0] # fast path for string with no template fragment "x"
224 224 else:
225 225 return (op,) + xs
226 226
227 227 def parse(tmpl):
228 228 """Parse template string into tree"""
229 229 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
230 230 assert pos == len(tmpl), 'unquoted template should be consumed'
231 231 return _unnesttemplatelist(('template', parsed))
232 232
233 233 def _parseexpr(expr):
234 234 """Parse a template expression into tree
235 235
236 236 >>> _parseexpr(b'"foo"')
237 237 ('string', 'foo')
238 238 >>> _parseexpr(b'foo(bar)')
239 239 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
240 240 >>> _parseexpr(b'foo(')
241 241 Traceback (most recent call last):
242 242 ...
243 243 ParseError: ('not a prefix: end', 4)
244 244 >>> _parseexpr(b'"foo" "bar"')
245 245 Traceback (most recent call last):
246 246 ...
247 247 ParseError: ('invalid token', 7)
248 248 """
249 249 p = parser.parser(elements)
250 250 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
251 251 if pos != len(expr):
252 252 raise error.ParseError(_('invalid token'), pos)
253 253 return _unnesttemplatelist(tree)
254 254
255 255 def prettyformat(tree):
256 256 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
257 257
258 258 def compileexp(exp, context, curmethods):
259 259 """Compile parsed template tree to (func, data) pair"""
260 260 t = exp[0]
261 261 if t in curmethods:
262 262 return curmethods[t](exp, context)
263 263 raise error.ParseError(_("unknown method '%s'") % t)
264 264
265 265 # template evaluation
266 266
267 267 def getsymbol(exp):
268 268 if exp[0] == 'symbol':
269 269 return exp[1]
270 270 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
271 271
272 272 def getlist(x):
273 273 if not x:
274 274 return []
275 275 if x[0] == 'list':
276 276 return getlist(x[1]) + [x[2]]
277 277 return [x]
278 278
279 279 def gettemplate(exp, context):
280 280 """Compile given template tree or load named template from map file;
281 281 returns (func, data) pair"""
282 282 if exp[0] in ('template', 'string'):
283 283 return compileexp(exp, context, methods)
284 284 if exp[0] == 'symbol':
285 285 # unlike runsymbol(), here 'symbol' is always taken as template name
286 286 # even if it exists in mapping. this allows us to override mapping
287 287 # by web templates, e.g. 'changelogtag' is redefined in map file.
288 288 return context._load(exp[1])
289 289 raise error.ParseError(_("expected template specifier"))
290 290
291 291 def findsymbolicname(arg):
292 292 """Find symbolic name for the given compiled expression; returns None
293 293 if nothing found reliably"""
294 294 while True:
295 295 func, data = arg
296 296 if func is runsymbol:
297 297 return data
298 298 elif func is runfilter:
299 299 arg = data[0]
300 300 else:
301 301 return None
302 302
303 303 def evalrawexp(context, mapping, arg):
304 304 """Evaluate given argument as a bare template object which may require
305 305 further processing (such as folding generator of strings)"""
306 306 func, data = arg
307 307 return func(context, mapping, data)
308 308
309 309 def evalfuncarg(context, mapping, arg):
310 310 """Evaluate given argument as value type"""
311 311 thing = evalrawexp(context, mapping, arg)
312 312 thing = templatekw.unwrapvalue(thing)
313 313 # evalrawexp() may return string, generator of strings or arbitrary object
314 314 # such as date tuple, but filter does not want generator.
315 315 if isinstance(thing, types.GeneratorType):
316 316 thing = stringify(thing)
317 317 return thing
318 318
319 319 def evalboolean(context, mapping, arg):
320 320 """Evaluate given argument as boolean, but also takes boolean literals"""
321 321 func, data = arg
322 322 if func is runsymbol:
323 323 thing = func(context, mapping, data, default=None)
324 324 if thing is None:
325 325 # not a template keyword, takes as a boolean literal
326 326 thing = util.parsebool(data)
327 327 else:
328 328 thing = func(context, mapping, data)
329 329 thing = templatekw.unwrapvalue(thing)
330 330 if isinstance(thing, bool):
331 331 return thing
332 332 # other objects are evaluated as strings, which means 0 is True, but
333 333 # empty dict/list should be False as they are expected to be ''
334 334 return bool(stringify(thing))
335 335
336 336 def evalinteger(context, mapping, arg, err=None):
337 337 v = evalfuncarg(context, mapping, arg)
338 338 try:
339 339 return int(v)
340 340 except (TypeError, ValueError):
341 341 raise error.ParseError(err or _('not an integer'))
342 342
343 343 def evalstring(context, mapping, arg):
344 344 return stringify(evalrawexp(context, mapping, arg))
345 345
346 346 def evalstringliteral(context, mapping, arg):
347 347 """Evaluate given argument as string template, but returns symbol name
348 348 if it is unknown"""
349 349 func, data = arg
350 350 if func is runsymbol:
351 351 thing = func(context, mapping, data, default=data)
352 352 else:
353 353 thing = func(context, mapping, data)
354 354 return stringify(thing)
355 355
356 356 _evalfuncbytype = {
357 357 bool: evalboolean,
358 358 bytes: evalstring,
359 359 int: evalinteger,
360 360 }
361 361
362 362 def evalastype(context, mapping, arg, typ):
363 363 """Evaluate given argument and coerce its type"""
364 364 try:
365 365 f = _evalfuncbytype[typ]
366 366 except KeyError:
367 367 raise error.ProgrammingError('invalid type specified: %r' % typ)
368 368 return f(context, mapping, arg)
369 369
370 370 def runinteger(context, mapping, data):
371 371 return int(data)
372 372
373 373 def runstring(context, mapping, data):
374 374 return data
375 375
376 376 def _recursivesymbolblocker(key):
377 377 def showrecursion(**args):
378 378 raise error.Abort(_("recursive reference '%s' in template") % key)
379 379 return showrecursion
380 380
381 381 def _runrecursivesymbol(context, mapping, key):
382 382 raise error.Abort(_("recursive reference '%s' in template") % key)
383 383
384 384 def runsymbol(context, mapping, key, default=''):
385 385 v = mapping.get(key)
386 386 if v is None:
387 387 v = context._defaults.get(key)
388 388 if v is None:
389 389 # put poison to cut recursion. we can't move this to parsing phase
390 390 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
391 391 safemapping = mapping.copy()
392 392 safemapping[key] = _recursivesymbolblocker(key)
393 393 try:
394 394 v = context.process(key, safemapping)
395 395 except TemplateNotFound:
396 396 v = default
397 397 if callable(v):
398 398 return v(**pycompat.strkwargs(mapping))
399 399 return v
400 400
401 401 def buildtemplate(exp, context):
402 402 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
403 403 return (runtemplate, ctmpl)
404 404
405 405 def runtemplate(context, mapping, template):
406 406 for arg in template:
407 407 yield evalrawexp(context, mapping, arg)
408 408
409 409 def buildfilter(exp, context):
410 410 n = getsymbol(exp[2])
411 411 if n in context._filters:
412 412 filt = context._filters[n]
413 413 arg = compileexp(exp[1], context, methods)
414 414 return (runfilter, (arg, filt))
415 415 if n in funcs:
416 416 f = funcs[n]
417 417 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
418 418 return (f, args)
419 419 raise error.ParseError(_("unknown function '%s'") % n)
420 420
421 421 def runfilter(context, mapping, data):
422 422 arg, filt = data
423 423 thing = evalfuncarg(context, mapping, arg)
424 424 try:
425 425 return filt(thing)
426 426 except (ValueError, AttributeError, TypeError):
427 427 sym = findsymbolicname(arg)
428 428 if sym:
429 429 msg = (_("template filter '%s' is not compatible with keyword '%s'")
430 % (filt.func_name, sym))
430 % (filt.__name__, sym))
431 431 else:
432 msg = _("incompatible use of template filter '%s'") % filt.func_name
432 msg = _("incompatible use of template filter '%s'") % filt.__name__
433 433 raise error.Abort(msg)
434 434
435 435 def buildmap(exp, context):
436 436 darg = compileexp(exp[1], context, methods)
437 437 targ = gettemplate(exp[2], context)
438 438 return (runmap, (darg, targ))
439 439
440 440 def runmap(context, mapping, data):
441 441 darg, targ = data
442 442 d = evalrawexp(context, mapping, darg)
443 443 if util.safehasattr(d, 'itermaps'):
444 444 diter = d.itermaps()
445 445 else:
446 446 try:
447 447 diter = iter(d)
448 448 except TypeError:
449 449 sym = findsymbolicname(darg)
450 450 if sym:
451 451 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
452 452 else:
453 453 raise error.ParseError(_("%r is not iterable") % d)
454 454
455 455 for i, v in enumerate(diter):
456 456 lm = mapping.copy()
457 457 lm['index'] = i
458 458 if isinstance(v, dict):
459 459 lm.update(v)
460 460 lm['originalnode'] = mapping.get('node')
461 461 yield evalrawexp(context, lm, targ)
462 462 else:
463 463 # v is not an iterable of dicts, this happen when 'key'
464 464 # has been fully expanded already and format is useless.
465 465 # If so, return the expanded value.
466 466 yield v
467 467
468 468 def buildmember(exp, context):
469 469 darg = compileexp(exp[1], context, methods)
470 470 memb = getsymbol(exp[2])
471 471 return (runmember, (darg, memb))
472 472
473 473 def runmember(context, mapping, data):
474 474 darg, memb = data
475 475 d = evalrawexp(context, mapping, darg)
476 476 if util.safehasattr(d, 'tomap'):
477 477 lm = mapping.copy()
478 478 lm.update(d.tomap())
479 479 return runsymbol(context, lm, memb)
480 480 if util.safehasattr(d, 'get'):
481 481 return _getdictitem(d, memb)
482 482
483 483 sym = findsymbolicname(darg)
484 484 if sym:
485 485 raise error.ParseError(_("keyword '%s' has no member") % sym)
486 486 else:
487 487 raise error.ParseError(_("%r has no member") % d)
488 488
489 489 def buildnegate(exp, context):
490 490 arg = compileexp(exp[1], context, exprmethods)
491 491 return (runnegate, arg)
492 492
493 493 def runnegate(context, mapping, data):
494 494 data = evalinteger(context, mapping, data,
495 495 _('negation needs an integer argument'))
496 496 return -data
497 497
498 498 def buildarithmetic(exp, context, func):
499 499 left = compileexp(exp[1], context, exprmethods)
500 500 right = compileexp(exp[2], context, exprmethods)
501 501 return (runarithmetic, (func, left, right))
502 502
503 503 def runarithmetic(context, mapping, data):
504 504 func, left, right = data
505 505 left = evalinteger(context, mapping, left,
506 506 _('arithmetic only defined on integers'))
507 507 right = evalinteger(context, mapping, right,
508 508 _('arithmetic only defined on integers'))
509 509 try:
510 510 return func(left, right)
511 511 except ZeroDivisionError:
512 512 raise error.Abort(_('division by zero is not defined'))
513 513
514 514 def buildfunc(exp, context):
515 515 n = getsymbol(exp[1])
516 516 if n in funcs:
517 517 f = funcs[n]
518 518 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
519 519 return (f, args)
520 520 if n in context._filters:
521 521 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
522 522 if len(args) != 1:
523 523 raise error.ParseError(_("filter %s expects one argument") % n)
524 524 f = context._filters[n]
525 525 return (runfilter, (args[0], f))
526 526 raise error.ParseError(_("unknown function '%s'") % n)
527 527
528 528 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
529 529 """Compile parsed tree of function arguments into list or dict of
530 530 (func, data) pairs
531 531
532 532 >>> context = engine(lambda t: (runsymbol, t))
533 533 >>> def fargs(expr, argspec):
534 534 ... x = _parseexpr(expr)
535 535 ... n = getsymbol(x[1])
536 536 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
537 537 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
538 538 ['l', 'k']
539 539 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
540 540 >>> list(args.keys()), list(args[b'opts'].keys())
541 541 (['opts'], ['opts', 'k'])
542 542 """
543 543 def compiledict(xs):
544 544 return util.sortdict((k, compileexp(x, context, curmethods))
545 545 for k, x in xs.iteritems())
546 546 def compilelist(xs):
547 547 return [compileexp(x, context, curmethods) for x in xs]
548 548
549 549 if not argspec:
550 550 # filter or function with no argspec: return list of positional args
551 551 return compilelist(getlist(exp))
552 552
553 553 # function with argspec: return dict of named args
554 554 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
555 555 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
556 556 keyvaluenode='keyvalue', keynode='symbol')
557 557 compargs = util.sortdict()
558 558 if varkey:
559 559 compargs[varkey] = compilelist(treeargs.pop(varkey))
560 560 if optkey:
561 561 compargs[optkey] = compiledict(treeargs.pop(optkey))
562 562 compargs.update(compiledict(treeargs))
563 563 return compargs
564 564
565 565 def buildkeyvaluepair(exp, content):
566 566 raise error.ParseError(_("can't use a key-value pair in this context"))
567 567
568 568 # dict of template built-in functions
569 569 funcs = {}
570 570
571 571 templatefunc = registrar.templatefunc(funcs)
572 572
573 573 @templatefunc('date(date[, fmt])')
574 574 def date(context, mapping, args):
575 575 """Format a date. See :hg:`help dates` for formatting
576 576 strings. The default is a Unix date format, including the timezone:
577 577 "Mon Sep 04 15:13:13 2006 0700"."""
578 578 if not (1 <= len(args) <= 2):
579 579 # i18n: "date" is a keyword
580 580 raise error.ParseError(_("date expects one or two arguments"))
581 581
582 582 date = evalfuncarg(context, mapping, args[0])
583 583 fmt = None
584 584 if len(args) == 2:
585 585 fmt = evalstring(context, mapping, args[1])
586 586 try:
587 587 if fmt is None:
588 588 return util.datestr(date)
589 589 else:
590 590 return util.datestr(date, fmt)
591 591 except (TypeError, ValueError):
592 592 # i18n: "date" is a keyword
593 593 raise error.ParseError(_("date expects a date information"))
594 594
595 595 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
596 596 def dict_(context, mapping, args):
597 597 """Construct a dict from key-value pairs. A key may be omitted if
598 598 a value expression can provide an unambiguous name."""
599 599 data = util.sortdict()
600 600
601 601 for v in args['args']:
602 602 k = findsymbolicname(v)
603 603 if not k:
604 604 raise error.ParseError(_('dict key cannot be inferred'))
605 605 if k in data or k in args['kwargs']:
606 606 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
607 607 data[k] = evalfuncarg(context, mapping, v)
608 608
609 609 data.update((k, evalfuncarg(context, mapping, v))
610 610 for k, v in args['kwargs'].iteritems())
611 611 return templatekw.hybriddict(data)
612 612
613 613 @templatefunc('diff([includepattern [, excludepattern]])')
614 614 def diff(context, mapping, args):
615 615 """Show a diff, optionally
616 616 specifying files to include or exclude."""
617 617 if len(args) > 2:
618 618 # i18n: "diff" is a keyword
619 619 raise error.ParseError(_("diff expects zero, one, or two arguments"))
620 620
621 621 def getpatterns(i):
622 622 if i < len(args):
623 623 s = evalstring(context, mapping, args[i]).strip()
624 624 if s:
625 625 return [s]
626 626 return []
627 627
628 628 ctx = mapping['ctx']
629 629 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
630 630
631 631 return ''.join(chunks)
632 632
633 633 @templatefunc('extdata(source)', argspec='source')
634 634 def extdata(context, mapping, args):
635 635 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
636 636 if 'source' not in args:
637 637 # i18n: "extdata" is a keyword
638 638 raise error.ParseError(_('extdata expects one argument'))
639 639
640 640 source = evalstring(context, mapping, args['source'])
641 641 cache = mapping['cache'].setdefault('extdata', {})
642 642 ctx = mapping['ctx']
643 643 if source in cache:
644 644 data = cache[source]
645 645 else:
646 646 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
647 647 return data.get(ctx.rev(), '')
648 648
649 649 @templatefunc('files(pattern)')
650 650 def files(context, mapping, args):
651 651 """All files of the current changeset matching the pattern. See
652 652 :hg:`help patterns`."""
653 653 if not len(args) == 1:
654 654 # i18n: "files" is a keyword
655 655 raise error.ParseError(_("files expects one argument"))
656 656
657 657 raw = evalstring(context, mapping, args[0])
658 658 ctx = mapping['ctx']
659 659 m = ctx.match([raw])
660 660 files = list(ctx.matches(m))
661 661 return templatekw.showlist("file", files, mapping)
662 662
663 663 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
664 664 def fill(context, mapping, args):
665 665 """Fill many
666 666 paragraphs with optional indentation. See the "fill" filter."""
667 667 if not (1 <= len(args) <= 4):
668 668 # i18n: "fill" is a keyword
669 669 raise error.ParseError(_("fill expects one to four arguments"))
670 670
671 671 text = evalstring(context, mapping, args[0])
672 672 width = 76
673 673 initindent = ''
674 674 hangindent = ''
675 675 if 2 <= len(args) <= 4:
676 676 width = evalinteger(context, mapping, args[1],
677 677 # i18n: "fill" is a keyword
678 678 _("fill expects an integer width"))
679 679 try:
680 680 initindent = evalstring(context, mapping, args[2])
681 681 hangindent = evalstring(context, mapping, args[3])
682 682 except IndexError:
683 683 pass
684 684
685 685 return templatefilters.fill(text, width, initindent, hangindent)
686 686
687 687 @templatefunc('formatnode(node)')
688 688 def formatnode(context, mapping, args):
689 689 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
690 690 if len(args) != 1:
691 691 # i18n: "formatnode" is a keyword
692 692 raise error.ParseError(_("formatnode expects one argument"))
693 693
694 694 ui = mapping['ui']
695 695 node = evalstring(context, mapping, args[0])
696 696 if ui.debugflag:
697 697 return node
698 698 return templatefilters.short(node)
699 699
700 700 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
701 701 argspec='text width fillchar left')
702 702 def pad(context, mapping, args):
703 703 """Pad text with a
704 704 fill character."""
705 705 if 'text' not in args or 'width' not in args:
706 706 # i18n: "pad" is a keyword
707 707 raise error.ParseError(_("pad() expects two to four arguments"))
708 708
709 709 width = evalinteger(context, mapping, args['width'],
710 710 # i18n: "pad" is a keyword
711 711 _("pad() expects an integer width"))
712 712
713 713 text = evalstring(context, mapping, args['text'])
714 714
715 715 left = False
716 716 fillchar = ' '
717 717 if 'fillchar' in args:
718 718 fillchar = evalstring(context, mapping, args['fillchar'])
719 719 if len(color.stripeffects(fillchar)) != 1:
720 720 # i18n: "pad" is a keyword
721 721 raise error.ParseError(_("pad() expects a single fill character"))
722 722 if 'left' in args:
723 723 left = evalboolean(context, mapping, args['left'])
724 724
725 725 fillwidth = width - encoding.colwidth(color.stripeffects(text))
726 726 if fillwidth <= 0:
727 727 return text
728 728 if left:
729 729 return fillchar * fillwidth + text
730 730 else:
731 731 return text + fillchar * fillwidth
732 732
733 733 @templatefunc('indent(text, indentchars[, firstline])')
734 734 def indent(context, mapping, args):
735 735 """Indents all non-empty lines
736 736 with the characters given in the indentchars string. An optional
737 737 third parameter will override the indent for the first line only
738 738 if present."""
739 739 if not (2 <= len(args) <= 3):
740 740 # i18n: "indent" is a keyword
741 741 raise error.ParseError(_("indent() expects two or three arguments"))
742 742
743 743 text = evalstring(context, mapping, args[0])
744 744 indent = evalstring(context, mapping, args[1])
745 745
746 746 if len(args) == 3:
747 747 firstline = evalstring(context, mapping, args[2])
748 748 else:
749 749 firstline = indent
750 750
751 751 # the indent function doesn't indent the first line, so we do it here
752 752 return templatefilters.indent(firstline + text, indent)
753 753
754 754 @templatefunc('get(dict, key)')
755 755 def get(context, mapping, args):
756 756 """Get an attribute/key from an object. Some keywords
757 757 are complex types. This function allows you to obtain the value of an
758 758 attribute on these types."""
759 759 if len(args) != 2:
760 760 # i18n: "get" is a keyword
761 761 raise error.ParseError(_("get() expects two arguments"))
762 762
763 763 dictarg = evalfuncarg(context, mapping, args[0])
764 764 if not util.safehasattr(dictarg, 'get'):
765 765 # i18n: "get" is a keyword
766 766 raise error.ParseError(_("get() expects a dict as first argument"))
767 767
768 768 key = evalfuncarg(context, mapping, args[1])
769 769 return _getdictitem(dictarg, key)
770 770
771 771 def _getdictitem(dictarg, key):
772 772 val = dictarg.get(key)
773 773 if val is None:
774 774 return
775 775 return templatekw.wraphybridvalue(dictarg, key, val)
776 776
777 777 @templatefunc('if(expr, then[, else])')
778 778 def if_(context, mapping, args):
779 779 """Conditionally execute based on the result of
780 780 an expression."""
781 781 if not (2 <= len(args) <= 3):
782 782 # i18n: "if" is a keyword
783 783 raise error.ParseError(_("if expects two or three arguments"))
784 784
785 785 test = evalboolean(context, mapping, args[0])
786 786 if test:
787 787 yield evalrawexp(context, mapping, args[1])
788 788 elif len(args) == 3:
789 789 yield evalrawexp(context, mapping, args[2])
790 790
791 791 @templatefunc('ifcontains(needle, haystack, then[, else])')
792 792 def ifcontains(context, mapping, args):
793 793 """Conditionally execute based
794 794 on whether the item "needle" is in "haystack"."""
795 795 if not (3 <= len(args) <= 4):
796 796 # i18n: "ifcontains" is a keyword
797 797 raise error.ParseError(_("ifcontains expects three or four arguments"))
798 798
799 799 haystack = evalfuncarg(context, mapping, args[1])
800 800 try:
801 801 needle = evalastype(context, mapping, args[0],
802 802 getattr(haystack, 'keytype', None) or bytes)
803 803 found = (needle in haystack)
804 804 except error.ParseError:
805 805 found = False
806 806
807 807 if found:
808 808 yield evalrawexp(context, mapping, args[2])
809 809 elif len(args) == 4:
810 810 yield evalrawexp(context, mapping, args[3])
811 811
812 812 @templatefunc('ifeq(expr1, expr2, then[, else])')
813 813 def ifeq(context, mapping, args):
814 814 """Conditionally execute based on
815 815 whether 2 items are equivalent."""
816 816 if not (3 <= len(args) <= 4):
817 817 # i18n: "ifeq" is a keyword
818 818 raise error.ParseError(_("ifeq expects three or four arguments"))
819 819
820 820 test = evalstring(context, mapping, args[0])
821 821 match = evalstring(context, mapping, args[1])
822 822 if test == match:
823 823 yield evalrawexp(context, mapping, args[2])
824 824 elif len(args) == 4:
825 825 yield evalrawexp(context, mapping, args[3])
826 826
827 827 @templatefunc('join(list, sep)')
828 828 def join(context, mapping, args):
829 829 """Join items in a list with a delimiter."""
830 830 if not (1 <= len(args) <= 2):
831 831 # i18n: "join" is a keyword
832 832 raise error.ParseError(_("join expects one or two arguments"))
833 833
834 834 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
835 835 # abuses generator as a keyword that returns a list of dicts.
836 836 joinset = evalrawexp(context, mapping, args[0])
837 837 joinset = templatekw.unwrapvalue(joinset)
838 838 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
839 839 joiner = " "
840 840 if len(args) > 1:
841 841 joiner = evalstring(context, mapping, args[1])
842 842
843 843 first = True
844 844 for x in joinset:
845 845 if first:
846 846 first = False
847 847 else:
848 848 yield joiner
849 849 yield joinfmt(x)
850 850
851 851 @templatefunc('label(label, expr)')
852 852 def label(context, mapping, args):
853 853 """Apply a label to generated content. Content with
854 854 a label applied can result in additional post-processing, such as
855 855 automatic colorization."""
856 856 if len(args) != 2:
857 857 # i18n: "label" is a keyword
858 858 raise error.ParseError(_("label expects two arguments"))
859 859
860 860 ui = mapping['ui']
861 861 thing = evalstring(context, mapping, args[1])
862 862 # preserve unknown symbol as literal so effects like 'red', 'bold',
863 863 # etc. don't need to be quoted
864 864 label = evalstringliteral(context, mapping, args[0])
865 865
866 866 return ui.label(thing, label)
867 867
868 868 @templatefunc('latesttag([pattern])')
869 869 def latesttag(context, mapping, args):
870 870 """The global tags matching the given pattern on the
871 871 most recent globally tagged ancestor of this changeset.
872 872 If no such tags exist, the "{tag}" template resolves to
873 873 the string "null"."""
874 874 if len(args) > 1:
875 875 # i18n: "latesttag" is a keyword
876 876 raise error.ParseError(_("latesttag expects at most one argument"))
877 877
878 878 pattern = None
879 879 if len(args) == 1:
880 880 pattern = evalstring(context, mapping, args[0])
881 881
882 882 return templatekw.showlatesttags(pattern, **mapping)
883 883
884 884 @templatefunc('localdate(date[, tz])')
885 885 def localdate(context, mapping, args):
886 886 """Converts a date to the specified timezone.
887 887 The default is local date."""
888 888 if not (1 <= len(args) <= 2):
889 889 # i18n: "localdate" is a keyword
890 890 raise error.ParseError(_("localdate expects one or two arguments"))
891 891
892 892 date = evalfuncarg(context, mapping, args[0])
893 893 try:
894 894 date = util.parsedate(date)
895 895 except AttributeError: # not str nor date tuple
896 896 # i18n: "localdate" is a keyword
897 897 raise error.ParseError(_("localdate expects a date information"))
898 898 if len(args) >= 2:
899 899 tzoffset = None
900 900 tz = evalfuncarg(context, mapping, args[1])
901 901 if isinstance(tz, str):
902 902 tzoffset, remainder = util.parsetimezone(tz)
903 903 if remainder:
904 904 tzoffset = None
905 905 if tzoffset is None:
906 906 try:
907 907 tzoffset = int(tz)
908 908 except (TypeError, ValueError):
909 909 # i18n: "localdate" is a keyword
910 910 raise error.ParseError(_("localdate expects a timezone"))
911 911 else:
912 912 tzoffset = util.makedate()[1]
913 913 return (date[0], tzoffset)
914 914
915 915 @templatefunc('max(iterable)')
916 916 def max_(context, mapping, args, **kwargs):
917 917 """Return the max of an iterable"""
918 918 if len(args) != 1:
919 919 # i18n: "max" is a keyword
920 920 raise error.ParseError(_("max expects one arguments"))
921 921
922 922 iterable = evalfuncarg(context, mapping, args[0])
923 923 try:
924 924 x = max(iterable)
925 925 except (TypeError, ValueError):
926 926 # i18n: "max" is a keyword
927 927 raise error.ParseError(_("max first argument should be an iterable"))
928 928 return templatekw.wraphybridvalue(iterable, x, x)
929 929
930 930 @templatefunc('min(iterable)')
931 931 def min_(context, mapping, args, **kwargs):
932 932 """Return the min of an iterable"""
933 933 if len(args) != 1:
934 934 # i18n: "min" is a keyword
935 935 raise error.ParseError(_("min expects one arguments"))
936 936
937 937 iterable = evalfuncarg(context, mapping, args[0])
938 938 try:
939 939 x = min(iterable)
940 940 except (TypeError, ValueError):
941 941 # i18n: "min" is a keyword
942 942 raise error.ParseError(_("min first argument should be an iterable"))
943 943 return templatekw.wraphybridvalue(iterable, x, x)
944 944
945 945 @templatefunc('mod(a, b)')
946 946 def mod(context, mapping, args):
947 947 """Calculate a mod b such that a / b + a mod b == a"""
948 948 if not len(args) == 2:
949 949 # i18n: "mod" is a keyword
950 950 raise error.ParseError(_("mod expects two arguments"))
951 951
952 952 func = lambda a, b: a % b
953 953 return runarithmetic(context, mapping, (func, args[0], args[1]))
954 954
955 955 @templatefunc('obsfateoperations(markers)')
956 956 def obsfateoperations(context, mapping, args):
957 957 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
958 958 if len(args) != 1:
959 959 # i18n: "obsfateoperations" is a keyword
960 960 raise error.ParseError(_("obsfateoperations expects one arguments"))
961 961
962 962 markers = evalfuncarg(context, mapping, args[0])
963 963
964 964 try:
965 965 data = obsutil.markersoperations(markers)
966 966 return templatekw.hybridlist(data, name='operation')
967 967 except (TypeError, KeyError):
968 968 # i18n: "obsfateoperations" is a keyword
969 969 errmsg = _("obsfateoperations first argument should be an iterable")
970 970 raise error.ParseError(errmsg)
971 971
972 972 @templatefunc('obsfatedate(markers)')
973 973 def obsfatedate(context, mapping, args):
974 974 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
975 975 if len(args) != 1:
976 976 # i18n: "obsfatedate" is a keyword
977 977 raise error.ParseError(_("obsfatedate expects one arguments"))
978 978
979 979 markers = evalfuncarg(context, mapping, args[0])
980 980
981 981 try:
982 982 data = obsutil.markersdates(markers)
983 983 return templatekw.hybridlist(data, name='date', fmt='%d %d')
984 984 except (TypeError, KeyError):
985 985 # i18n: "obsfatedate" is a keyword
986 986 errmsg = _("obsfatedate first argument should be an iterable")
987 987 raise error.ParseError(errmsg)
988 988
989 989 @templatefunc('obsfateusers(markers)')
990 990 def obsfateusers(context, mapping, args):
991 991 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
992 992 if len(args) != 1:
993 993 # i18n: "obsfateusers" is a keyword
994 994 raise error.ParseError(_("obsfateusers expects one arguments"))
995 995
996 996 markers = evalfuncarg(context, mapping, args[0])
997 997
998 998 try:
999 999 data = obsutil.markersusers(markers)
1000 1000 return templatekw.hybridlist(data, name='user')
1001 1001 except (TypeError, KeyError, ValueError):
1002 1002 # i18n: "obsfateusers" is a keyword
1003 1003 msg = _("obsfateusers first argument should be an iterable of "
1004 1004 "obsmakers")
1005 1005 raise error.ParseError(msg)
1006 1006
1007 1007 @templatefunc('obsfateverb(successors)')
1008 1008 def obsfateverb(context, mapping, args):
1009 1009 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1010 1010 if len(args) != 1:
1011 1011 # i18n: "obsfateverb" is a keyword
1012 1012 raise error.ParseError(_("obsfateverb expects one arguments"))
1013 1013
1014 1014 successors = evalfuncarg(context, mapping, args[0])
1015 1015
1016 1016 try:
1017 1017 return obsutil.successorsetverb(successors)
1018 1018 except TypeError:
1019 1019 # i18n: "obsfateverb" is a keyword
1020 1020 errmsg = _("obsfateverb first argument should be countable")
1021 1021 raise error.ParseError(errmsg)
1022 1022
1023 1023 @templatefunc('relpath(path)')
1024 1024 def relpath(context, mapping, args):
1025 1025 """Convert a repository-absolute path into a filesystem path relative to
1026 1026 the current working directory."""
1027 1027 if len(args) != 1:
1028 1028 # i18n: "relpath" is a keyword
1029 1029 raise error.ParseError(_("relpath expects one argument"))
1030 1030
1031 1031 repo = mapping['ctx'].repo()
1032 1032 path = evalstring(context, mapping, args[0])
1033 1033 return repo.pathto(path)
1034 1034
1035 1035 @templatefunc('revset(query[, formatargs...])')
1036 1036 def revset(context, mapping, args):
1037 1037 """Execute a revision set query. See
1038 1038 :hg:`help revset`."""
1039 1039 if not len(args) > 0:
1040 1040 # i18n: "revset" is a keyword
1041 1041 raise error.ParseError(_("revset expects one or more arguments"))
1042 1042
1043 1043 raw = evalstring(context, mapping, args[0])
1044 1044 ctx = mapping['ctx']
1045 1045 repo = ctx.repo()
1046 1046
1047 1047 def query(expr):
1048 1048 m = revsetmod.match(repo.ui, expr, repo=repo)
1049 1049 return m(repo)
1050 1050
1051 1051 if len(args) > 1:
1052 1052 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1053 1053 revs = query(revsetlang.formatspec(raw, *formatargs))
1054 1054 revs = list(revs)
1055 1055 else:
1056 1056 revsetcache = mapping['cache'].setdefault("revsetcache", {})
1057 1057 if raw in revsetcache:
1058 1058 revs = revsetcache[raw]
1059 1059 else:
1060 1060 revs = query(raw)
1061 1061 revs = list(revs)
1062 1062 revsetcache[raw] = revs
1063 1063
1064 1064 return templatekw.showrevslist("revision", revs, **mapping)
1065 1065
1066 1066 @templatefunc('rstdoc(text, style)')
1067 1067 def rstdoc(context, mapping, args):
1068 1068 """Format reStructuredText."""
1069 1069 if len(args) != 2:
1070 1070 # i18n: "rstdoc" is a keyword
1071 1071 raise error.ParseError(_("rstdoc expects two arguments"))
1072 1072
1073 1073 text = evalstring(context, mapping, args[0])
1074 1074 style = evalstring(context, mapping, args[1])
1075 1075
1076 1076 return minirst.format(text, style=style, keep=['verbose'])
1077 1077
1078 1078 @templatefunc('separate(sep, args)', argspec='sep *args')
1079 1079 def separate(context, mapping, args):
1080 1080 """Add a separator between non-empty arguments."""
1081 1081 if 'sep' not in args:
1082 1082 # i18n: "separate" is a keyword
1083 1083 raise error.ParseError(_("separate expects at least one argument"))
1084 1084
1085 1085 sep = evalstring(context, mapping, args['sep'])
1086 1086 first = True
1087 1087 for arg in args['args']:
1088 1088 argstr = evalstring(context, mapping, arg)
1089 1089 if not argstr:
1090 1090 continue
1091 1091 if first:
1092 1092 first = False
1093 1093 else:
1094 1094 yield sep
1095 1095 yield argstr
1096 1096
1097 1097 @templatefunc('shortest(node, minlength=4)')
1098 1098 def shortest(context, mapping, args):
1099 1099 """Obtain the shortest representation of
1100 1100 a node."""
1101 1101 if not (1 <= len(args) <= 2):
1102 1102 # i18n: "shortest" is a keyword
1103 1103 raise error.ParseError(_("shortest() expects one or two arguments"))
1104 1104
1105 1105 node = evalstring(context, mapping, args[0])
1106 1106
1107 1107 minlength = 4
1108 1108 if len(args) > 1:
1109 1109 minlength = evalinteger(context, mapping, args[1],
1110 1110 # i18n: "shortest" is a keyword
1111 1111 _("shortest() expects an integer minlength"))
1112 1112
1113 1113 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1114 1114 # which would be unacceptably slow. so we look for hash collision in
1115 1115 # unfiltered space, which means some hashes may be slightly longer.
1116 1116 cl = mapping['ctx']._repo.unfiltered().changelog
1117 1117 return cl.shortest(node, minlength)
1118 1118
1119 1119 @templatefunc('strip(text[, chars])')
1120 1120 def strip(context, mapping, args):
1121 1121 """Strip characters from a string. By default,
1122 1122 strips all leading and trailing whitespace."""
1123 1123 if not (1 <= len(args) <= 2):
1124 1124 # i18n: "strip" is a keyword
1125 1125 raise error.ParseError(_("strip expects one or two arguments"))
1126 1126
1127 1127 text = evalstring(context, mapping, args[0])
1128 1128 if len(args) == 2:
1129 1129 chars = evalstring(context, mapping, args[1])
1130 1130 return text.strip(chars)
1131 1131 return text.strip()
1132 1132
1133 1133 @templatefunc('sub(pattern, replacement, expression)')
1134 1134 def sub(context, mapping, args):
1135 1135 """Perform text substitution
1136 1136 using regular expressions."""
1137 1137 if len(args) != 3:
1138 1138 # i18n: "sub" is a keyword
1139 1139 raise error.ParseError(_("sub expects three arguments"))
1140 1140
1141 1141 pat = evalstring(context, mapping, args[0])
1142 1142 rpl = evalstring(context, mapping, args[1])
1143 1143 src = evalstring(context, mapping, args[2])
1144 1144 try:
1145 1145 patre = re.compile(pat)
1146 1146 except re.error:
1147 1147 # i18n: "sub" is a keyword
1148 1148 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1149 1149 try:
1150 1150 yield patre.sub(rpl, src)
1151 1151 except re.error:
1152 1152 # i18n: "sub" is a keyword
1153 1153 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1154 1154
1155 1155 @templatefunc('startswith(pattern, text)')
1156 1156 def startswith(context, mapping, args):
1157 1157 """Returns the value from the "text" argument
1158 1158 if it begins with the content from the "pattern" argument."""
1159 1159 if len(args) != 2:
1160 1160 # i18n: "startswith" is a keyword
1161 1161 raise error.ParseError(_("startswith expects two arguments"))
1162 1162
1163 1163 patn = evalstring(context, mapping, args[0])
1164 1164 text = evalstring(context, mapping, args[1])
1165 1165 if text.startswith(patn):
1166 1166 return text
1167 1167 return ''
1168 1168
1169 1169 @templatefunc('word(number, text[, separator])')
1170 1170 def word(context, mapping, args):
1171 1171 """Return the nth word from a string."""
1172 1172 if not (2 <= len(args) <= 3):
1173 1173 # i18n: "word" is a keyword
1174 1174 raise error.ParseError(_("word expects two or three arguments, got %d")
1175 1175 % len(args))
1176 1176
1177 1177 num = evalinteger(context, mapping, args[0],
1178 1178 # i18n: "word" is a keyword
1179 1179 _("word expects an integer index"))
1180 1180 text = evalstring(context, mapping, args[1])
1181 1181 if len(args) == 3:
1182 1182 splitter = evalstring(context, mapping, args[2])
1183 1183 else:
1184 1184 splitter = None
1185 1185
1186 1186 tokens = text.split(splitter)
1187 1187 if num >= len(tokens) or num < -len(tokens):
1188 1188 return ''
1189 1189 else:
1190 1190 return tokens[num]
1191 1191
1192 1192 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1193 1193 exprmethods = {
1194 1194 "integer": lambda e, c: (runinteger, e[1]),
1195 1195 "string": lambda e, c: (runstring, e[1]),
1196 1196 "symbol": lambda e, c: (runsymbol, e[1]),
1197 1197 "template": buildtemplate,
1198 1198 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1199 1199 ".": buildmember,
1200 1200 "|": buildfilter,
1201 1201 "%": buildmap,
1202 1202 "func": buildfunc,
1203 1203 "keyvalue": buildkeyvaluepair,
1204 1204 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1205 1205 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1206 1206 "negate": buildnegate,
1207 1207 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1208 1208 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1209 1209 }
1210 1210
1211 1211 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1212 1212 methods = exprmethods.copy()
1213 1213 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1214 1214
1215 1215 class _aliasrules(parser.basealiasrules):
1216 1216 """Parsing and expansion rule set of template aliases"""
1217 1217 _section = _('template alias')
1218 1218 _parse = staticmethod(_parseexpr)
1219 1219
1220 1220 @staticmethod
1221 1221 def _trygetfunc(tree):
1222 1222 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1223 1223 None"""
1224 1224 if tree[0] == 'func' and tree[1][0] == 'symbol':
1225 1225 return tree[1][1], getlist(tree[2])
1226 1226 if tree[0] == '|' and tree[2][0] == 'symbol':
1227 1227 return tree[2][1], [tree[1]]
1228 1228
1229 1229 def expandaliases(tree, aliases):
1230 1230 """Return new tree of aliases are expanded"""
1231 1231 aliasmap = _aliasrules.buildmap(aliases)
1232 1232 return _aliasrules.expand(aliasmap, tree)
1233 1233
1234 1234 # template engine
1235 1235
1236 1236 stringify = templatefilters.stringify
1237 1237
1238 1238 def _flatten(thing):
1239 1239 '''yield a single stream from a possibly nested set of iterators'''
1240 1240 thing = templatekw.unwraphybrid(thing)
1241 1241 if isinstance(thing, bytes):
1242 1242 yield thing
1243 1243 elif thing is None:
1244 1244 pass
1245 1245 elif not util.safehasattr(thing, '__iter__'):
1246 1246 yield pycompat.bytestr(thing)
1247 1247 else:
1248 1248 for i in thing:
1249 1249 i = templatekw.unwraphybrid(i)
1250 1250 if isinstance(i, bytes):
1251 1251 yield i
1252 1252 elif i is None:
1253 1253 pass
1254 1254 elif not util.safehasattr(i, '__iter__'):
1255 1255 yield pycompat.bytestr(i)
1256 1256 else:
1257 1257 for j in _flatten(i):
1258 1258 yield j
1259 1259
1260 1260 def unquotestring(s):
1261 1261 '''unwrap quotes if any; otherwise returns unmodified string'''
1262 1262 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1263 1263 return s
1264 1264 return s[1:-1]
1265 1265
1266 1266 class engine(object):
1267 1267 '''template expansion engine.
1268 1268
1269 1269 template expansion works like this. a map file contains key=value
1270 1270 pairs. if value is quoted, it is treated as string. otherwise, it
1271 1271 is treated as name of template file.
1272 1272
1273 1273 templater is asked to expand a key in map. it looks up key, and
1274 1274 looks for strings like this: {foo}. it expands {foo} by looking up
1275 1275 foo in map, and substituting it. expansion is recursive: it stops
1276 1276 when there is no more {foo} to replace.
1277 1277
1278 1278 expansion also allows formatting and filtering.
1279 1279
1280 1280 format uses key to expand each item in list. syntax is
1281 1281 {key%format}.
1282 1282
1283 1283 filter uses function to transform value. syntax is
1284 1284 {key|filter1|filter2|...}.'''
1285 1285
1286 1286 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1287 1287 self._loader = loader
1288 1288 if filters is None:
1289 1289 filters = {}
1290 1290 self._filters = filters
1291 1291 if defaults is None:
1292 1292 defaults = {}
1293 1293 self._defaults = defaults
1294 1294 self._aliasmap = _aliasrules.buildmap(aliases)
1295 1295 self._cache = {} # key: (func, data)
1296 1296
1297 1297 def _load(self, t):
1298 1298 '''load, parse, and cache a template'''
1299 1299 if t not in self._cache:
1300 1300 # put poison to cut recursion while compiling 't'
1301 1301 self._cache[t] = (_runrecursivesymbol, t)
1302 1302 try:
1303 1303 x = parse(self._loader(t))
1304 1304 if self._aliasmap:
1305 1305 x = _aliasrules.expand(self._aliasmap, x)
1306 1306 self._cache[t] = compileexp(x, self, methods)
1307 1307 except: # re-raises
1308 1308 del self._cache[t]
1309 1309 raise
1310 1310 return self._cache[t]
1311 1311
1312 1312 def process(self, t, mapping):
1313 1313 '''Perform expansion. t is name of map element to expand.
1314 1314 mapping contains added elements for use during expansion. Is a
1315 1315 generator.'''
1316 1316 func, data = self._load(t)
1317 1317 return _flatten(func(self, mapping, data))
1318 1318
1319 1319 engines = {'default': engine}
1320 1320
1321 1321 def stylelist():
1322 1322 paths = templatepaths()
1323 1323 if not paths:
1324 1324 return _('no templates found, try `hg debuginstall` for more info')
1325 1325 dirlist = os.listdir(paths[0])
1326 1326 stylelist = []
1327 1327 for file in dirlist:
1328 1328 split = file.split(".")
1329 1329 if split[-1] in ('orig', 'rej'):
1330 1330 continue
1331 1331 if split[0] == "map-cmdline":
1332 1332 stylelist.append(split[1])
1333 1333 return ", ".join(sorted(stylelist))
1334 1334
1335 1335 def _readmapfile(mapfile):
1336 1336 """Load template elements from the given map file"""
1337 1337 if not os.path.exists(mapfile):
1338 1338 raise error.Abort(_("style '%s' not found") % mapfile,
1339 1339 hint=_("available styles: %s") % stylelist())
1340 1340
1341 1341 base = os.path.dirname(mapfile)
1342 1342 conf = config.config(includepaths=templatepaths())
1343 1343 conf.read(mapfile)
1344 1344
1345 1345 cache = {}
1346 1346 tmap = {}
1347 1347 for key, val in conf[''].items():
1348 1348 if not val:
1349 1349 raise error.ParseError(_('missing value'), conf.source('', key))
1350 1350 if val[0] in "'\"":
1351 1351 if val[0] != val[-1]:
1352 1352 raise error.ParseError(_('unmatched quotes'),
1353 1353 conf.source('', key))
1354 1354 cache[key] = unquotestring(val)
1355 1355 elif key == "__base__":
1356 1356 # treat as a pointer to a base class for this style
1357 1357 path = util.normpath(os.path.join(base, val))
1358 1358
1359 1359 # fallback check in template paths
1360 1360 if not os.path.exists(path):
1361 1361 for p in templatepaths():
1362 1362 p2 = util.normpath(os.path.join(p, val))
1363 1363 if os.path.isfile(p2):
1364 1364 path = p2
1365 1365 break
1366 1366 p3 = util.normpath(os.path.join(p2, "map"))
1367 1367 if os.path.isfile(p3):
1368 1368 path = p3
1369 1369 break
1370 1370
1371 1371 bcache, btmap = _readmapfile(path)
1372 1372 for k in bcache:
1373 1373 if k not in cache:
1374 1374 cache[k] = bcache[k]
1375 1375 for k in btmap:
1376 1376 if k not in tmap:
1377 1377 tmap[k] = btmap[k]
1378 1378 else:
1379 1379 val = 'default', val
1380 1380 if ':' in val[1]:
1381 1381 val = val[1].split(':', 1)
1382 1382 tmap[key] = val[0], os.path.join(base, val[1])
1383 1383 return cache, tmap
1384 1384
1385 1385 class TemplateNotFound(error.Abort):
1386 1386 pass
1387 1387
1388 1388 class templater(object):
1389 1389
1390 1390 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1391 1391 minchunk=1024, maxchunk=65536):
1392 1392 '''set up template engine.
1393 1393 filters is dict of functions. each transforms a value into another.
1394 1394 defaults is dict of default map definitions.
1395 1395 aliases is list of alias (name, replacement) pairs.
1396 1396 '''
1397 1397 if filters is None:
1398 1398 filters = {}
1399 1399 if defaults is None:
1400 1400 defaults = {}
1401 1401 if cache is None:
1402 1402 cache = {}
1403 1403 self.cache = cache.copy()
1404 1404 self.map = {}
1405 1405 self.filters = templatefilters.filters.copy()
1406 1406 self.filters.update(filters)
1407 1407 self.defaults = defaults
1408 1408 self._aliases = aliases
1409 1409 self.minchunk, self.maxchunk = minchunk, maxchunk
1410 1410 self.ecache = {}
1411 1411
1412 1412 @classmethod
1413 1413 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1414 1414 minchunk=1024, maxchunk=65536):
1415 1415 """Create templater from the specified map file"""
1416 1416 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1417 1417 cache, tmap = _readmapfile(mapfile)
1418 1418 t.cache.update(cache)
1419 1419 t.map = tmap
1420 1420 return t
1421 1421
1422 1422 def __contains__(self, key):
1423 1423 return key in self.cache or key in self.map
1424 1424
1425 1425 def load(self, t):
1426 1426 '''Get the template for the given template name. Use a local cache.'''
1427 1427 if t not in self.cache:
1428 1428 try:
1429 1429 self.cache[t] = util.readfile(self.map[t][1])
1430 1430 except KeyError as inst:
1431 1431 raise TemplateNotFound(_('"%s" not in template map') %
1432 1432 inst.args[0])
1433 1433 except IOError as inst:
1434 1434 raise IOError(inst.args[0], _('template file %s: %s') %
1435 1435 (self.map[t][1], inst.args[1]))
1436 1436 return self.cache[t]
1437 1437
1438 1438 def render(self, mapping):
1439 1439 """Render the default unnamed template and return result as string"""
1440 1440 mapping = pycompat.strkwargs(mapping)
1441 1441 return stringify(self('', **mapping))
1442 1442
1443 1443 def __call__(self, t, **mapping):
1444 1444 mapping = pycompat.byteskwargs(mapping)
1445 1445 ttype = t in self.map and self.map[t][0] or 'default'
1446 1446 if ttype not in self.ecache:
1447 1447 try:
1448 1448 ecls = engines[ttype]
1449 1449 except KeyError:
1450 1450 raise error.Abort(_('invalid template engine: %s') % ttype)
1451 1451 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1452 1452 self._aliases)
1453 1453 proc = self.ecache[ttype]
1454 1454
1455 1455 stream = proc.process(t, mapping)
1456 1456 if self.minchunk:
1457 1457 stream = util.increasingchunks(stream, min=self.minchunk,
1458 1458 max=self.maxchunk)
1459 1459 return stream
1460 1460
1461 1461 def templatepaths():
1462 1462 '''return locations used for template files.'''
1463 1463 pathsrel = ['templates']
1464 1464 paths = [os.path.normpath(os.path.join(util.datapath, f))
1465 1465 for f in pathsrel]
1466 1466 return [p for p in paths if os.path.isdir(p)]
1467 1467
1468 1468 def templatepath(name):
1469 1469 '''return location of template file. returns None if not found.'''
1470 1470 for p in templatepaths():
1471 1471 f = os.path.join(p, name)
1472 1472 if os.path.exists(f):
1473 1473 return f
1474 1474 return None
1475 1475
1476 1476 def stylemap(styles, paths=None):
1477 1477 """Return path to mapfile for a given style.
1478 1478
1479 1479 Searches mapfile in the following locations:
1480 1480 1. templatepath/style/map
1481 1481 2. templatepath/map-style
1482 1482 3. templatepath/map
1483 1483 """
1484 1484
1485 1485 if paths is None:
1486 1486 paths = templatepaths()
1487 1487 elif isinstance(paths, str):
1488 1488 paths = [paths]
1489 1489
1490 1490 if isinstance(styles, str):
1491 1491 styles = [styles]
1492 1492
1493 1493 for style in styles:
1494 1494 # only plain name is allowed to honor template paths
1495 1495 if (not style
1496 1496 or style in (os.curdir, os.pardir)
1497 1497 or pycompat.ossep in style
1498 1498 or pycompat.osaltsep and pycompat.osaltsep in style):
1499 1499 continue
1500 1500 locations = [os.path.join(style, 'map'), 'map-' + style]
1501 1501 locations.append('map')
1502 1502
1503 1503 for path in paths:
1504 1504 for location in locations:
1505 1505 mapfile = os.path.join(path, location)
1506 1506 if os.path.isfile(mapfile):
1507 1507 return style, mapfile
1508 1508
1509 1509 raise RuntimeError("No hgweb templates found in %r" % paths)
1510 1510
1511 1511 def loadfunction(ui, extname, registrarobj):
1512 1512 """Load template function from specified registrarobj
1513 1513 """
1514 1514 for name, func in registrarobj._table.iteritems():
1515 1515 funcs[name] = func
1516 1516
1517 1517 # tell hggettext to extract docstrings from these functions:
1518 1518 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now