##// END OF EJS Templates
templater: drop symbols which should be overridden by new 'ctx' (issue5612)...
Yuya Nishihara -
r37093:46859b43 default
parent child Browse files
Show More
@@ -1,588 +1,592
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 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 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.verbose = verbose
49 49 ... ui.pushbuffer()
50 50 ... try:
51 51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 52 ... pycompat.byteskwargs(opts)))
53 53 ... finally:
54 54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55 55
56 56 Basic example:
57 57
58 58 >>> def files(ui, fm):
59 59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 60 ... for f in files:
61 61 ... fm.startitem()
62 62 ... fm.write(b'path', b'%s', f[0])
63 63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 65 ... fm.data(size=f[1])
66 66 ... fm.plain(b'\\n')
67 67 ... fm.end()
68 68 >>> show(files)
69 69 foo
70 70 bar
71 71 >>> show(files, verbose=True)
72 72 foo 1970-01-01 00:00:00
73 73 bar 1970-01-01 00:00:01
74 74 >>> show(files, template=b'json')
75 75 [
76 76 {
77 77 "date": [0, 0],
78 78 "path": "foo",
79 79 "size": 123
80 80 },
81 81 {
82 82 "date": [1, 0],
83 83 "path": "bar",
84 84 "size": 456
85 85 }
86 86 ]
87 87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 88 path: foo
89 89 date: 1970-01-01T00:00:00+00:00
90 90 path: bar
91 91 date: 1970-01-01T00:00:01+00:00
92 92
93 93 Nested example:
94 94
95 95 >>> def subrepos(ui, fm):
96 96 ... fm.startitem()
97 97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 98 ... files(ui, fm.nested(b'files'))
99 99 ... fm.end()
100 100 >>> show(subrepos)
101 101 [baz]
102 102 foo
103 103 bar
104 104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 105 baz: foo, bar
106 106 """
107 107
108 108 from __future__ import absolute_import, print_function
109 109
110 110 import collections
111 111 import contextlib
112 112 import itertools
113 113 import os
114 114
115 115 from .i18n import _
116 116 from .node import (
117 117 hex,
118 118 short,
119 119 )
120 120
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 templatefilters,
125 125 templatekw,
126 126 templater,
127 127 templateutil,
128 128 util,
129 129 )
130 130 from .utils import dateutil
131 131
132 132 pickle = util.pickle
133 133
134 134 class _nullconverter(object):
135 135 '''convert non-primitive data types to be processed by formatter'''
136 136
137 137 # set to True if context object should be stored as item
138 138 storecontext = False
139 139
140 140 @staticmethod
141 141 def formatdate(date, fmt):
142 142 '''convert date tuple to appropriate format'''
143 143 return date
144 144 @staticmethod
145 145 def formatdict(data, key, value, fmt, sep):
146 146 '''convert dict or key-value pairs to appropriate dict format'''
147 147 # use plain dict instead of util.sortdict so that data can be
148 148 # serialized as a builtin dict in pickle output
149 149 return dict(data)
150 150 @staticmethod
151 151 def formatlist(data, name, fmt, sep):
152 152 '''convert iterable to appropriate list format'''
153 153 return list(data)
154 154
155 155 class baseformatter(object):
156 156 def __init__(self, ui, topic, opts, converter):
157 157 self._ui = ui
158 158 self._topic = topic
159 159 self._style = opts.get("style")
160 160 self._template = opts.get("template")
161 161 self._converter = converter
162 162 self._item = None
163 163 # function to convert node to string suitable for this output
164 164 self.hexfunc = hex
165 165 def __enter__(self):
166 166 return self
167 167 def __exit__(self, exctype, excvalue, traceback):
168 168 if exctype is None:
169 169 self.end()
170 170 def _showitem(self):
171 171 '''show a formatted item once all data is collected'''
172 172 def startitem(self):
173 173 '''begin an item in the format list'''
174 174 if self._item is not None:
175 175 self._showitem()
176 176 self._item = {}
177 177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
178 178 '''convert date tuple to appropriate format'''
179 179 return self._converter.formatdate(date, fmt)
180 180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
181 181 '''convert dict or key-value pairs to appropriate dict format'''
182 182 return self._converter.formatdict(data, key, value, fmt, sep)
183 183 def formatlist(self, data, name, fmt=None, sep=' '):
184 184 '''convert iterable to appropriate list format'''
185 185 # name is mandatory argument for now, but it could be optional if
186 186 # we have default template keyword, e.g. {item}
187 187 return self._converter.formatlist(data, name, fmt, sep)
188 188 def context(self, **ctxs):
189 189 '''insert context objects to be used to render template keywords'''
190 190 ctxs = pycompat.byteskwargs(ctxs)
191 191 assert all(k in {'ctx', 'fctx'} for k in ctxs)
192 192 if self._converter.storecontext:
193 193 self._item.update(ctxs)
194 194 def data(self, **data):
195 195 '''insert data into item that's not shown in default output'''
196 196 data = pycompat.byteskwargs(data)
197 197 self._item.update(data)
198 198 def write(self, fields, deftext, *fielddata, **opts):
199 199 '''do default text output while assigning data to item'''
200 200 fieldkeys = fields.split()
201 201 assert len(fieldkeys) == len(fielddata)
202 202 self._item.update(zip(fieldkeys, fielddata))
203 203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
204 204 '''do conditional write (primarily for plain formatter)'''
205 205 fieldkeys = fields.split()
206 206 assert len(fieldkeys) == len(fielddata)
207 207 self._item.update(zip(fieldkeys, fielddata))
208 208 def plain(self, text, **opts):
209 209 '''show raw text for non-templated mode'''
210 210 def isplain(self):
211 211 '''check for plain formatter usage'''
212 212 return False
213 213 def nested(self, field):
214 214 '''sub formatter to store nested data in the specified field'''
215 215 self._item[field] = data = []
216 216 return _nestedformatter(self._ui, self._converter, data)
217 217 def end(self):
218 218 '''end output for the formatter'''
219 219 if self._item is not None:
220 220 self._showitem()
221 221
222 222 def nullformatter(ui, topic):
223 223 '''formatter that prints nothing'''
224 224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
225 225
226 226 class _nestedformatter(baseformatter):
227 227 '''build sub items and store them in the parent formatter'''
228 228 def __init__(self, ui, converter, data):
229 229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
230 230 self._data = data
231 231 def _showitem(self):
232 232 self._data.append(self._item)
233 233
234 234 def _iteritems(data):
235 235 '''iterate key-value pairs in stable order'''
236 236 if isinstance(data, dict):
237 237 return sorted(data.iteritems())
238 238 return data
239 239
240 240 class _plainconverter(object):
241 241 '''convert non-primitive data types to text'''
242 242
243 243 storecontext = False
244 244
245 245 @staticmethod
246 246 def formatdate(date, fmt):
247 247 '''stringify date tuple in the given format'''
248 248 return dateutil.datestr(date, fmt)
249 249 @staticmethod
250 250 def formatdict(data, key, value, fmt, sep):
251 251 '''stringify key-value pairs separated by sep'''
252 252 prefmt = pycompat.identity
253 253 if fmt is None:
254 254 fmt = '%s=%s'
255 255 prefmt = pycompat.bytestr
256 256 return sep.join(fmt % (prefmt(k), prefmt(v))
257 257 for k, v in _iteritems(data))
258 258 @staticmethod
259 259 def formatlist(data, name, fmt, sep):
260 260 '''stringify iterable separated by sep'''
261 261 prefmt = pycompat.identity
262 262 if fmt is None:
263 263 fmt = '%s'
264 264 prefmt = pycompat.bytestr
265 265 return sep.join(fmt % prefmt(e) for e in data)
266 266
267 267 class plainformatter(baseformatter):
268 268 '''the default text output scheme'''
269 269 def __init__(self, ui, out, topic, opts):
270 270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
271 271 if ui.debugflag:
272 272 self.hexfunc = hex
273 273 else:
274 274 self.hexfunc = short
275 275 if ui is out:
276 276 self._write = ui.write
277 277 else:
278 278 self._write = lambda s, **opts: out.write(s)
279 279 def startitem(self):
280 280 pass
281 281 def data(self, **data):
282 282 pass
283 283 def write(self, fields, deftext, *fielddata, **opts):
284 284 self._write(deftext % fielddata, **opts)
285 285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
286 286 '''do conditional write'''
287 287 if cond:
288 288 self._write(deftext % fielddata, **opts)
289 289 def plain(self, text, **opts):
290 290 self._write(text, **opts)
291 291 def isplain(self):
292 292 return True
293 293 def nested(self, field):
294 294 # nested data will be directly written to ui
295 295 return self
296 296 def end(self):
297 297 pass
298 298
299 299 class debugformatter(baseformatter):
300 300 def __init__(self, ui, out, topic, opts):
301 301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 302 self._out = out
303 303 self._out.write("%s = [\n" % self._topic)
304 304 def _showitem(self):
305 305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
306 306 def end(self):
307 307 baseformatter.end(self)
308 308 self._out.write("]\n")
309 309
310 310 class pickleformatter(baseformatter):
311 311 def __init__(self, ui, out, topic, opts):
312 312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 313 self._out = out
314 314 self._data = []
315 315 def _showitem(self):
316 316 self._data.append(self._item)
317 317 def end(self):
318 318 baseformatter.end(self)
319 319 self._out.write(pickle.dumps(self._data))
320 320
321 321 class jsonformatter(baseformatter):
322 322 def __init__(self, ui, out, topic, opts):
323 323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
324 324 self._out = out
325 325 self._out.write("[")
326 326 self._first = True
327 327 def _showitem(self):
328 328 if self._first:
329 329 self._first = False
330 330 else:
331 331 self._out.write(",")
332 332
333 333 self._out.write("\n {\n")
334 334 first = True
335 335 for k, v in sorted(self._item.items()):
336 336 if first:
337 337 first = False
338 338 else:
339 339 self._out.write(",\n")
340 340 u = templatefilters.json(v, paranoid=False)
341 341 self._out.write(' "%s": %s' % (k, u))
342 342 self._out.write("\n }")
343 343 def end(self):
344 344 baseformatter.end(self)
345 345 self._out.write("\n]\n")
346 346
347 347 class _templateconverter(object):
348 348 '''convert non-primitive data types to be processed by templater'''
349 349
350 350 storecontext = True
351 351
352 352 @staticmethod
353 353 def formatdate(date, fmt):
354 354 '''return date tuple'''
355 355 return date
356 356 @staticmethod
357 357 def formatdict(data, key, value, fmt, sep):
358 358 '''build object that can be evaluated as either plain string or dict'''
359 359 data = util.sortdict(_iteritems(data))
360 360 def f():
361 361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
362 362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
363 363 gen=f)
364 364 @staticmethod
365 365 def formatlist(data, name, fmt, sep):
366 366 '''build object that can be evaluated as either plain string or list'''
367 367 data = list(data)
368 368 def f():
369 369 yield _plainconverter.formatlist(data, name, fmt, sep)
370 370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
371 371
372 372 class templateformatter(baseformatter):
373 373 def __init__(self, ui, out, topic, opts):
374 374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
375 375 self._out = out
376 376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
377 377 self._tref = spec.ref
378 378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
379 379 resources=templateresources(ui),
380 380 cache=templatekw.defaulttempl)
381 381 self._parts = templatepartsmap(spec, self._t,
382 382 ['docheader', 'docfooter', 'separator'])
383 383 self._counter = itertools.count()
384 384 self._renderitem('docheader', {})
385 385
386 386 def _showitem(self):
387 387 item = self._item.copy()
388 388 item['index'] = index = next(self._counter)
389 389 if index > 0:
390 390 self._renderitem('separator', {})
391 391 self._renderitem(self._tref, item)
392 392
393 393 def _renderitem(self, part, item):
394 394 if part not in self._parts:
395 395 return
396 396 ref = self._parts[part]
397 397
398 398 props = {}
399 399 # explicitly-defined fields precede templatekw
400 400 props.update(item)
401 401 if 'ctx' in item or 'fctx' in item:
402 402 # but template resources must be always available
403 403 props['revcache'] = {}
404 404 self._out.write(self._t.render(ref, props))
405 405
406 406 def end(self):
407 407 baseformatter.end(self)
408 408 self._renderitem('docfooter', {})
409 409
410 410 templatespec = collections.namedtuple(r'templatespec',
411 411 r'ref tmpl mapfile')
412 412
413 413 def lookuptemplate(ui, topic, tmpl):
414 414 """Find the template matching the given -T/--template spec 'tmpl'
415 415
416 416 'tmpl' can be any of the following:
417 417
418 418 - a literal template (e.g. '{rev}')
419 419 - a map-file name or path (e.g. 'changelog')
420 420 - a reference to [templates] in config file
421 421 - a path to raw template file
422 422
423 423 A map file defines a stand-alone template environment. If a map file
424 424 selected, all templates defined in the file will be loaded, and the
425 425 template matching the given topic will be rendered. Aliases won't be
426 426 loaded from user config, but from the map file.
427 427
428 428 If no map file selected, all templates in [templates] section will be
429 429 available as well as aliases in [templatealias].
430 430 """
431 431
432 432 # looks like a literal template?
433 433 if '{' in tmpl:
434 434 return templatespec('', tmpl, None)
435 435
436 436 # perhaps a stock style?
437 437 if not os.path.split(tmpl)[0]:
438 438 mapname = (templater.templatepath('map-cmdline.' + tmpl)
439 439 or templater.templatepath(tmpl))
440 440 if mapname and os.path.isfile(mapname):
441 441 return templatespec(topic, None, mapname)
442 442
443 443 # perhaps it's a reference to [templates]
444 444 if ui.config('templates', tmpl):
445 445 return templatespec(tmpl, None, None)
446 446
447 447 if tmpl == 'list':
448 448 ui.write(_("available styles: %s\n") % templater.stylelist())
449 449 raise error.Abort(_("specify a template"))
450 450
451 451 # perhaps it's a path to a map or a template
452 452 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
453 453 # is it a mapfile for a style?
454 454 if os.path.basename(tmpl).startswith("map-"):
455 455 return templatespec(topic, None, os.path.realpath(tmpl))
456 456 with util.posixfile(tmpl, 'rb') as f:
457 457 tmpl = f.read()
458 458 return templatespec('', tmpl, None)
459 459
460 460 # constant string?
461 461 return templatespec('', tmpl, None)
462 462
463 463 def templatepartsmap(spec, t, partnames):
464 464 """Create a mapping of {part: ref}"""
465 465 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
466 466 if spec.mapfile:
467 467 partsmap.update((p, p) for p in partnames if p in t)
468 468 elif spec.ref:
469 469 for part in partnames:
470 470 ref = '%s:%s' % (spec.ref, part) # select config sub-section
471 471 if ref in t:
472 472 partsmap[part] = ref
473 473 return partsmap
474 474
475 475 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
476 476 """Create a templater from either a literal template or loading from
477 477 a map file"""
478 478 assert not (spec.tmpl and spec.mapfile)
479 479 if spec.mapfile:
480 480 frommapfile = templater.templater.frommapfile
481 481 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
482 482 cache=cache)
483 483 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
484 484 cache=cache)
485 485
486 486 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
487 487 """Create a templater from a string template 'tmpl'"""
488 488 aliases = ui.configitems('templatealias')
489 489 t = templater.templater(defaults=defaults, resources=resources,
490 490 cache=cache, aliases=aliases)
491 491 t.cache.update((k, templater.unquotestring(v))
492 492 for k, v in ui.configitems('templates'))
493 493 if tmpl:
494 494 t.cache[''] = tmpl
495 495 return t
496 496
497 497 class templateresources(templater.resourcemapper):
498 498 """Resource mapper designed for the default templatekw and function"""
499 499
500 500 def __init__(self, ui, repo=None):
501 501 self._resmap = {
502 502 'cache': {}, # for templatekw/funcs to store reusable data
503 503 'repo': repo,
504 504 'ui': ui,
505 505 }
506 506
507 def availablekeys(self, context, mapping):
508 return {k for k, g in self._gettermap.iteritems()
509 if g(self, context, mapping, k) is not None}
510
507 511 def knownkeys(self):
508 512 return self._knownkeys
509 513
510 514 def lookup(self, context, mapping, key):
511 515 get = self._gettermap.get(key)
512 516 if not get:
513 517 return None
514 518 return get(self, context, mapping, key)
515 519
516 520 def _getsome(self, context, mapping, key):
517 521 v = mapping.get(key)
518 522 if v is not None:
519 523 return v
520 524 return self._resmap.get(key)
521 525
522 526 def _getctx(self, context, mapping, key):
523 527 ctx = mapping.get('ctx')
524 528 if ctx is not None:
525 529 return ctx
526 530 fctx = mapping.get('fctx')
527 531 if fctx is not None:
528 532 return fctx.changectx()
529 533
530 534 def _getrepo(self, context, mapping, key):
531 535 ctx = self._getctx(context, mapping, 'ctx')
532 536 if ctx is not None:
533 537 return ctx.repo()
534 538 return self._getsome(context, mapping, key)
535 539
536 540 _gettermap = {
537 541 'cache': _getsome,
538 542 'ctx': _getctx,
539 543 'fctx': _getsome,
540 544 'repo': _getrepo,
541 545 'revcache': _getsome, # per-ctx cache; set later
542 546 'ui': _getsome,
543 547 }
544 548 _knownkeys = set(_gettermap.keys())
545 549
546 550 def formatter(ui, out, topic, opts):
547 551 template = opts.get("template", "")
548 552 if template == "json":
549 553 return jsonformatter(ui, out, topic, opts)
550 554 elif template == "pickle":
551 555 return pickleformatter(ui, out, topic, opts)
552 556 elif template == "debug":
553 557 return debugformatter(ui, out, topic, opts)
554 558 elif template != "":
555 559 return templateformatter(ui, out, topic, opts)
556 560 # developer config: ui.formatdebug
557 561 elif ui.configbool('ui', 'formatdebug'):
558 562 return debugformatter(ui, out, topic, opts)
559 563 # deprecated config: ui.formatjson
560 564 elif ui.configbool('ui', 'formatjson'):
561 565 return jsonformatter(ui, out, topic, opts)
562 566 return plainformatter(ui, out, topic, opts)
563 567
564 568 @contextlib.contextmanager
565 569 def openformatter(ui, filename, topic, opts):
566 570 """Create a formatter that writes outputs to the specified file
567 571
568 572 Must be invoked using the 'with' statement.
569 573 """
570 574 with util.posixfile(filename, 'wb') as out:
571 575 with formatter(ui, out, topic, opts) as fm:
572 576 yield fm
573 577
574 578 @contextlib.contextmanager
575 579 def _neverending(fm):
576 580 yield fm
577 581
578 582 def maybereopen(fm, filename, opts):
579 583 """Create a formatter backed by file if filename specified, else return
580 584 the given formatter
581 585
582 586 Must be invoked using the 'with' statement. This will never call fm.end()
583 587 of the given formatter.
584 588 """
585 589 if filename:
586 590 return openformatter(fm._ui, filename, fm._topic, opts)
587 591 else:
588 592 return _neverending(fm)
@@ -1,873 +1,893
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 """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 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 30 returns a generator of dicts.)
31 31
32 32 None
33 33 sometimes represents an empty value, which can be stringified to ''.
34 34
35 35 True, False, int, float
36 36 can be stringified as such.
37 37
38 38 date tuple
39 39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
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 mappable
46 46 represents a scalar printable value, also supports % operator.
47 47 """
48 48
49 49 from __future__ import absolute_import, print_function
50 50
51 51 import abc
52 52 import os
53 53
54 54 from .i18n import _
55 55 from . import (
56 56 config,
57 57 encoding,
58 58 error,
59 59 parser,
60 60 pycompat,
61 61 templatefilters,
62 62 templatefuncs,
63 63 templateutil,
64 64 util,
65 65 )
66 66
67 67 # template parsing
68 68
69 69 elements = {
70 70 # token-type: binding-strength, primary, prefix, infix, suffix
71 71 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
72 72 ".": (18, None, None, (".", 18), None),
73 73 "%": (15, None, None, ("%", 15), None),
74 74 "|": (15, None, None, ("|", 15), None),
75 75 "*": (5, None, None, ("*", 5), None),
76 76 "/": (5, None, None, ("/", 5), None),
77 77 "+": (4, None, None, ("+", 4), None),
78 78 "-": (4, None, ("negate", 19), ("-", 4), None),
79 79 "=": (3, None, None, ("keyvalue", 3), None),
80 80 ",": (2, None, None, ("list", 2), None),
81 81 ")": (0, None, None, None, None),
82 82 "integer": (0, "integer", None, None, None),
83 83 "symbol": (0, "symbol", None, None, None),
84 84 "string": (0, "string", None, None, None),
85 85 "template": (0, "template", None, None, None),
86 86 "end": (0, None, None, None, None),
87 87 }
88 88
89 89 def tokenize(program, start, end, term=None):
90 90 """Parse a template expression into a stream of tokens, which must end
91 91 with term if specified"""
92 92 pos = start
93 93 program = pycompat.bytestr(program)
94 94 while pos < end:
95 95 c = program[pos]
96 96 if c.isspace(): # skip inter-token whitespace
97 97 pass
98 98 elif c in "(=,).%|+-*/": # handle simple operators
99 99 yield (c, None, pos)
100 100 elif c in '"\'': # handle quoted templates
101 101 s = pos + 1
102 102 data, pos = _parsetemplate(program, s, end, c)
103 103 yield ('template', data, s)
104 104 pos -= 1
105 105 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
106 106 # handle quoted strings
107 107 c = program[pos + 1]
108 108 s = pos = pos + 2
109 109 while pos < end: # find closing quote
110 110 d = program[pos]
111 111 if d == '\\': # skip over escaped characters
112 112 pos += 2
113 113 continue
114 114 if d == c:
115 115 yield ('string', program[s:pos], s)
116 116 break
117 117 pos += 1
118 118 else:
119 119 raise error.ParseError(_("unterminated string"), s)
120 120 elif c.isdigit():
121 121 s = pos
122 122 while pos < end:
123 123 d = program[pos]
124 124 if not d.isdigit():
125 125 break
126 126 pos += 1
127 127 yield ('integer', program[s:pos], s)
128 128 pos -= 1
129 129 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
130 130 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
131 131 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
132 132 # where some of nested templates were preprocessed as strings and
133 133 # then compiled. therefore, \"...\" was allowed. (issue4733)
134 134 #
135 135 # processing flow of _evalifliteral() at 5ab28a2e9962:
136 136 # outer template string -> stringify() -> compiletemplate()
137 137 # ------------------------ ------------ ------------------
138 138 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
139 139 # ~~~~~~~~
140 140 # escaped quoted string
141 141 if c == 'r':
142 142 pos += 1
143 143 token = 'string'
144 144 else:
145 145 token = 'template'
146 146 quote = program[pos:pos + 2]
147 147 s = pos = pos + 2
148 148 while pos < end: # find closing escaped quote
149 149 if program.startswith('\\\\\\', pos, end):
150 150 pos += 4 # skip over double escaped characters
151 151 continue
152 152 if program.startswith(quote, pos, end):
153 153 # interpret as if it were a part of an outer string
154 154 data = parser.unescapestr(program[s:pos])
155 155 if token == 'template':
156 156 data = _parsetemplate(data, 0, len(data))[0]
157 157 yield (token, data, s)
158 158 pos += 1
159 159 break
160 160 pos += 1
161 161 else:
162 162 raise error.ParseError(_("unterminated string"), s)
163 163 elif c.isalnum() or c in '_':
164 164 s = pos
165 165 pos += 1
166 166 while pos < end: # find end of symbol
167 167 d = program[pos]
168 168 if not (d.isalnum() or d == "_"):
169 169 break
170 170 pos += 1
171 171 sym = program[s:pos]
172 172 yield ('symbol', sym, s)
173 173 pos -= 1
174 174 elif c == term:
175 175 yield ('end', None, pos)
176 176 return
177 177 else:
178 178 raise error.ParseError(_("syntax error"), pos)
179 179 pos += 1
180 180 if term:
181 181 raise error.ParseError(_("unterminated template expansion"), start)
182 182 yield ('end', None, pos)
183 183
184 184 def _parsetemplate(tmpl, start, stop, quote=''):
185 185 r"""
186 186 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
187 187 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
188 188 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
189 189 ([('string', 'foo'), ('symbol', 'bar')], 9)
190 190 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
191 191 ([('string', 'foo')], 4)
192 192 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
193 193 ([('string', 'foo"'), ('string', 'bar')], 9)
194 194 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
195 195 ([('string', 'foo\\')], 6)
196 196 """
197 197 parsed = []
198 198 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
199 199 if typ == 'string':
200 200 parsed.append((typ, val))
201 201 elif typ == 'template':
202 202 parsed.append(val)
203 203 elif typ == 'end':
204 204 return parsed, pos
205 205 else:
206 206 raise error.ProgrammingError('unexpected type: %s' % typ)
207 207 raise error.ProgrammingError('unterminated scanning of template')
208 208
209 209 def scantemplate(tmpl, raw=False):
210 210 r"""Scan (type, start, end) positions of outermost elements in template
211 211
212 212 If raw=True, a backslash is not taken as an escape character just like
213 213 r'' string in Python. Note that this is different from r'' literal in
214 214 template in that no template fragment can appear in r'', e.g. r'{foo}'
215 215 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
216 216 'foo'.
217 217
218 218 >>> list(scantemplate(b'foo{bar}"baz'))
219 219 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
220 220 >>> list(scantemplate(b'outer{"inner"}outer'))
221 221 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
222 222 >>> list(scantemplate(b'foo\\{escaped}'))
223 223 [('string', 0, 5), ('string', 5, 13)]
224 224 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
225 225 [('string', 0, 4), ('template', 4, 13)]
226 226 """
227 227 last = None
228 228 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
229 229 if last:
230 230 yield last + (pos,)
231 231 if typ == 'end':
232 232 return
233 233 else:
234 234 last = (typ, pos)
235 235 raise error.ProgrammingError('unterminated scanning of template')
236 236
237 237 def _scantemplate(tmpl, start, stop, quote='', raw=False):
238 238 """Parse template string into chunks of strings and template expressions"""
239 239 sepchars = '{' + quote
240 240 unescape = [parser.unescapestr, pycompat.identity][raw]
241 241 pos = start
242 242 p = parser.parser(elements)
243 243 try:
244 244 while pos < stop:
245 245 n = min((tmpl.find(c, pos, stop) for c in sepchars),
246 246 key=lambda n: (n < 0, n))
247 247 if n < 0:
248 248 yield ('string', unescape(tmpl[pos:stop]), pos)
249 249 pos = stop
250 250 break
251 251 c = tmpl[n:n + 1]
252 252 bs = 0 # count leading backslashes
253 253 if not raw:
254 254 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
255 255 if bs % 2 == 1:
256 256 # escaped (e.g. '\{', '\\\{', but not '\\{')
257 257 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
258 258 pos = n + 1
259 259 continue
260 260 if n > pos:
261 261 yield ('string', unescape(tmpl[pos:n]), pos)
262 262 if c == quote:
263 263 yield ('end', None, n + 1)
264 264 return
265 265
266 266 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
267 267 if not tmpl.startswith('}', pos):
268 268 raise error.ParseError(_("invalid token"), pos)
269 269 yield ('template', parseres, n)
270 270 pos += 1
271 271
272 272 if quote:
273 273 raise error.ParseError(_("unterminated string"), start)
274 274 except error.ParseError as inst:
275 275 if len(inst.args) > 1: # has location
276 276 loc = inst.args[1]
277 277 # Offset the caret location by the number of newlines before the
278 278 # location of the error, since we will replace one-char newlines
279 279 # with the two-char literal r'\n'.
280 280 offset = tmpl[:loc].count('\n')
281 281 tmpl = tmpl.replace('\n', br'\n')
282 282 # We want the caret to point to the place in the template that
283 283 # failed to parse, but in a hint we get a open paren at the
284 284 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
285 285 # to line up the caret with the location of the error.
286 286 inst.hint = (tmpl + '\n'
287 287 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
288 288 raise
289 289 yield ('end', None, pos)
290 290
291 291 def _unnesttemplatelist(tree):
292 292 """Expand list of templates to node tuple
293 293
294 294 >>> def f(tree):
295 295 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
296 296 >>> f((b'template', []))
297 297 (string '')
298 298 >>> f((b'template', [(b'string', b'foo')]))
299 299 (string 'foo')
300 300 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
301 301 (template
302 302 (string 'foo')
303 303 (symbol 'rev'))
304 304 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
305 305 (template
306 306 (symbol 'rev'))
307 307 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
308 308 (string 'foo')
309 309 """
310 310 if not isinstance(tree, tuple):
311 311 return tree
312 312 op = tree[0]
313 313 if op != 'template':
314 314 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
315 315
316 316 assert len(tree) == 2
317 317 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
318 318 if not xs:
319 319 return ('string', '') # empty template ""
320 320 elif len(xs) == 1 and xs[0][0] == 'string':
321 321 return xs[0] # fast path for string with no template fragment "x"
322 322 else:
323 323 return (op,) + xs
324 324
325 325 def parse(tmpl):
326 326 """Parse template string into tree"""
327 327 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
328 328 assert pos == len(tmpl), 'unquoted template should be consumed'
329 329 return _unnesttemplatelist(('template', parsed))
330 330
331 331 def _parseexpr(expr):
332 332 """Parse a template expression into tree
333 333
334 334 >>> _parseexpr(b'"foo"')
335 335 ('string', 'foo')
336 336 >>> _parseexpr(b'foo(bar)')
337 337 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
338 338 >>> _parseexpr(b'foo(')
339 339 Traceback (most recent call last):
340 340 ...
341 341 ParseError: ('not a prefix: end', 4)
342 342 >>> _parseexpr(b'"foo" "bar"')
343 343 Traceback (most recent call last):
344 344 ...
345 345 ParseError: ('invalid token', 7)
346 346 """
347 347 p = parser.parser(elements)
348 348 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
349 349 if pos != len(expr):
350 350 raise error.ParseError(_('invalid token'), pos)
351 351 return _unnesttemplatelist(tree)
352 352
353 353 def prettyformat(tree):
354 354 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
355 355
356 356 def compileexp(exp, context, curmethods):
357 357 """Compile parsed template tree to (func, data) pair"""
358 358 if not exp:
359 359 raise error.ParseError(_("missing argument"))
360 360 t = exp[0]
361 361 if t in curmethods:
362 362 return curmethods[t](exp, context)
363 363 raise error.ParseError(_("unknown method '%s'") % t)
364 364
365 365 # template evaluation
366 366
367 367 def getsymbol(exp):
368 368 if exp[0] == 'symbol':
369 369 return exp[1]
370 370 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
371 371
372 372 def getlist(x):
373 373 if not x:
374 374 return []
375 375 if x[0] == 'list':
376 376 return getlist(x[1]) + [x[2]]
377 377 return [x]
378 378
379 379 def gettemplate(exp, context):
380 380 """Compile given template tree or load named template from map file;
381 381 returns (func, data) pair"""
382 382 if exp[0] in ('template', 'string'):
383 383 return compileexp(exp, context, methods)
384 384 if exp[0] == 'symbol':
385 385 # unlike runsymbol(), here 'symbol' is always taken as template name
386 386 # even if it exists in mapping. this allows us to override mapping
387 387 # by web templates, e.g. 'changelogtag' is redefined in map file.
388 388 return context._load(exp[1])
389 389 raise error.ParseError(_("expected template specifier"))
390 390
391 391 def _runrecursivesymbol(context, mapping, key):
392 392 raise error.Abort(_("recursive reference '%s' in template") % key)
393 393
394 394 def buildtemplate(exp, context):
395 395 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
396 396 return (templateutil.runtemplate, ctmpl)
397 397
398 398 def buildfilter(exp, context):
399 399 n = getsymbol(exp[2])
400 400 if n in context._filters:
401 401 filt = context._filters[n]
402 402 arg = compileexp(exp[1], context, methods)
403 403 return (templateutil.runfilter, (arg, filt))
404 404 if n in context._funcs:
405 405 f = context._funcs[n]
406 406 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
407 407 return (f, args)
408 408 raise error.ParseError(_("unknown function '%s'") % n)
409 409
410 410 def buildmap(exp, context):
411 411 darg = compileexp(exp[1], context, methods)
412 412 targ = gettemplate(exp[2], context)
413 413 return (templateutil.runmap, (darg, targ))
414 414
415 415 def buildmember(exp, context):
416 416 darg = compileexp(exp[1], context, methods)
417 417 memb = getsymbol(exp[2])
418 418 return (templateutil.runmember, (darg, memb))
419 419
420 420 def buildnegate(exp, context):
421 421 arg = compileexp(exp[1], context, exprmethods)
422 422 return (templateutil.runnegate, arg)
423 423
424 424 def buildarithmetic(exp, context, func):
425 425 left = compileexp(exp[1], context, exprmethods)
426 426 right = compileexp(exp[2], context, exprmethods)
427 427 return (templateutil.runarithmetic, (func, left, right))
428 428
429 429 def buildfunc(exp, context):
430 430 n = getsymbol(exp[1])
431 431 if n in context._funcs:
432 432 f = context._funcs[n]
433 433 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
434 434 return (f, args)
435 435 if n in context._filters:
436 436 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
437 437 if len(args) != 1:
438 438 raise error.ParseError(_("filter %s expects one argument") % n)
439 439 f = context._filters[n]
440 440 return (templateutil.runfilter, (args[0], f))
441 441 raise error.ParseError(_("unknown function '%s'") % n)
442 442
443 443 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
444 444 """Compile parsed tree of function arguments into list or dict of
445 445 (func, data) pairs
446 446
447 447 >>> context = engine(lambda t: (templateutil.runsymbol, t))
448 448 >>> def fargs(expr, argspec):
449 449 ... x = _parseexpr(expr)
450 450 ... n = getsymbol(x[1])
451 451 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
452 452 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
453 453 ['l', 'k']
454 454 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
455 455 >>> list(args.keys()), list(args[b'opts'].keys())
456 456 (['opts'], ['opts', 'k'])
457 457 """
458 458 def compiledict(xs):
459 459 return util.sortdict((k, compileexp(x, context, curmethods))
460 460 for k, x in xs.iteritems())
461 461 def compilelist(xs):
462 462 return [compileexp(x, context, curmethods) for x in xs]
463 463
464 464 if not argspec:
465 465 # filter or function with no argspec: return list of positional args
466 466 return compilelist(getlist(exp))
467 467
468 468 # function with argspec: return dict of named args
469 469 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
470 470 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
471 471 keyvaluenode='keyvalue', keynode='symbol')
472 472 compargs = util.sortdict()
473 473 if varkey:
474 474 compargs[varkey] = compilelist(treeargs.pop(varkey))
475 475 if optkey:
476 476 compargs[optkey] = compiledict(treeargs.pop(optkey))
477 477 compargs.update(compiledict(treeargs))
478 478 return compargs
479 479
480 480 def buildkeyvaluepair(exp, content):
481 481 raise error.ParseError(_("can't use a key-value pair in this context"))
482 482
483 483 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
484 484 exprmethods = {
485 485 "integer": lambda e, c: (templateutil.runinteger, e[1]),
486 486 "string": lambda e, c: (templateutil.runstring, e[1]),
487 487 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
488 488 "template": buildtemplate,
489 489 "group": lambda e, c: compileexp(e[1], c, exprmethods),
490 490 ".": buildmember,
491 491 "|": buildfilter,
492 492 "%": buildmap,
493 493 "func": buildfunc,
494 494 "keyvalue": buildkeyvaluepair,
495 495 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
496 496 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
497 497 "negate": buildnegate,
498 498 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
499 499 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
500 500 }
501 501
502 502 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
503 503 methods = exprmethods.copy()
504 504 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
505 505
506 506 class _aliasrules(parser.basealiasrules):
507 507 """Parsing and expansion rule set of template aliases"""
508 508 _section = _('template alias')
509 509 _parse = staticmethod(_parseexpr)
510 510
511 511 @staticmethod
512 512 def _trygetfunc(tree):
513 513 """Return (name, args) if tree is func(...) or ...|filter; otherwise
514 514 None"""
515 515 if tree[0] == 'func' and tree[1][0] == 'symbol':
516 516 return tree[1][1], getlist(tree[2])
517 517 if tree[0] == '|' and tree[2][0] == 'symbol':
518 518 return tree[2][1], [tree[1]]
519 519
520 520 def expandaliases(tree, aliases):
521 521 """Return new tree of aliases are expanded"""
522 522 aliasmap = _aliasrules.buildmap(aliases)
523 523 return _aliasrules.expand(aliasmap, tree)
524 524
525 525 # template engine
526 526
527 527 def _flatten(thing):
528 528 '''yield a single stream from a possibly nested set of iterators'''
529 529 thing = templateutil.unwraphybrid(thing)
530 530 if isinstance(thing, bytes):
531 531 yield thing
532 532 elif isinstance(thing, str):
533 533 # We can only hit this on Python 3, and it's here to guard
534 534 # against infinite recursion.
535 535 raise error.ProgrammingError('Mercurial IO including templates is done'
536 536 ' with bytes, not strings, got %r' % thing)
537 537 elif thing is None:
538 538 pass
539 539 elif not util.safehasattr(thing, '__iter__'):
540 540 yield pycompat.bytestr(thing)
541 541 else:
542 542 for i in thing:
543 543 i = templateutil.unwraphybrid(i)
544 544 if isinstance(i, bytes):
545 545 yield i
546 546 elif i is None:
547 547 pass
548 548 elif not util.safehasattr(i, '__iter__'):
549 549 yield pycompat.bytestr(i)
550 550 else:
551 551 for j in _flatten(i):
552 552 yield j
553 553
554 554 def unquotestring(s):
555 555 '''unwrap quotes if any; otherwise returns unmodified string'''
556 556 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
557 557 return s
558 558 return s[1:-1]
559 559
560 560 class resourcemapper(object):
561 561 """Mapper of internal template resources"""
562 562
563 563 __metaclass__ = abc.ABCMeta
564 564
565 565 @abc.abstractmethod
566 def availablekeys(self, context, mapping):
567 """Return a set of available resource keys based on the given mapping"""
568
569 @abc.abstractmethod
566 570 def knownkeys(self):
567 571 """Return a set of supported resource keys"""
568 572
569 573 @abc.abstractmethod
570 574 def lookup(self, context, mapping, key):
571 575 """Return a resource for the key if available; otherwise None"""
572 576
573 577 class nullresourcemapper(resourcemapper):
578 def availablekeys(self, context, mapping):
579 return set()
580
574 581 def knownkeys(self):
575 582 return set()
576 583
577 584 def lookup(self, context, mapping, key):
578 585 return None
579 586
580 587 class engine(object):
581 588 '''template expansion engine.
582 589
583 590 template expansion works like this. a map file contains key=value
584 591 pairs. if value is quoted, it is treated as string. otherwise, it
585 592 is treated as name of template file.
586 593
587 594 templater is asked to expand a key in map. it looks up key, and
588 595 looks for strings like this: {foo}. it expands {foo} by looking up
589 596 foo in map, and substituting it. expansion is recursive: it stops
590 597 when there is no more {foo} to replace.
591 598
592 599 expansion also allows formatting and filtering.
593 600
594 601 format uses key to expand each item in list. syntax is
595 602 {key%format}.
596 603
597 604 filter uses function to transform value. syntax is
598 605 {key|filter1|filter2|...}.'''
599 606
600 607 def __init__(self, loader, filters=None, defaults=None, resources=None,
601 608 aliases=()):
602 609 self._loader = loader
603 610 if filters is None:
604 611 filters = {}
605 612 self._filters = filters
606 613 self._funcs = templatefuncs.funcs # make this a parameter if needed
607 614 if defaults is None:
608 615 defaults = {}
609 616 if resources is None:
610 617 resources = nullresourcemapper()
611 618 self._defaults = defaults
612 619 self._resources = resources
613 620 self._aliasmap = _aliasrules.buildmap(aliases)
614 621 self._cache = {} # key: (func, data)
615 622
616 623 def overlaymap(self, origmapping, newmapping):
617 624 """Create combined mapping from the original mapping and partial
618 625 mapping to override the original"""
619 mapping = origmapping.copy()
626 # do not copy symbols which overrides the defaults depending on
627 # new resources, so the defaults will be re-evaluated (issue5612)
628 knownres = self._resources.knownkeys()
629 newres = self._resources.availablekeys(self, newmapping)
630 mapping = {k: v for k, v in origmapping.iteritems()
631 if (k in knownres # not a symbol per self.symbol()
632 or newres.isdisjoint(self._defaultrequires(k)))}
620 633 mapping.update(newmapping)
621 634 return mapping
622 635
636 def _defaultrequires(self, key):
637 """Resource keys required by the specified default symbol function"""
638 v = self._defaults.get(key)
639 if v is None or not callable(v):
640 return ()
641 return getattr(v, '_requires', ())
642
623 643 def symbol(self, mapping, key):
624 644 """Resolve symbol to value or function; None if nothing found"""
625 645 v = None
626 646 if key not in self._resources.knownkeys():
627 647 v = mapping.get(key)
628 648 if v is None:
629 649 v = self._defaults.get(key)
630 650 return v
631 651
632 652 def resource(self, mapping, key):
633 653 """Return internal data (e.g. cache) used for keyword/function
634 654 evaluation"""
635 655 v = self._resources.lookup(self, mapping, key)
636 656 if v is None:
637 657 raise templateutil.ResourceUnavailable(
638 658 _('template resource not available: %s') % key)
639 659 return v
640 660
641 661 def _load(self, t):
642 662 '''load, parse, and cache a template'''
643 663 if t not in self._cache:
644 664 # put poison to cut recursion while compiling 't'
645 665 self._cache[t] = (_runrecursivesymbol, t)
646 666 try:
647 667 x = parse(self._loader(t))
648 668 if self._aliasmap:
649 669 x = _aliasrules.expand(self._aliasmap, x)
650 670 self._cache[t] = compileexp(x, self, methods)
651 671 except: # re-raises
652 672 del self._cache[t]
653 673 raise
654 674 return self._cache[t]
655 675
656 676 def preload(self, t):
657 677 """Load, parse, and cache the specified template if available"""
658 678 try:
659 679 self._load(t)
660 680 return True
661 681 except templateutil.TemplateNotFound:
662 682 return False
663 683
664 684 def process(self, t, mapping):
665 685 '''Perform expansion. t is name of map element to expand.
666 686 mapping contains added elements for use during expansion. Is a
667 687 generator.'''
668 688 func, data = self._load(t)
669 689 return _flatten(func(self, mapping, data))
670 690
671 691 engines = {'default': engine}
672 692
673 693 def stylelist():
674 694 paths = templatepaths()
675 695 if not paths:
676 696 return _('no templates found, try `hg debuginstall` for more info')
677 697 dirlist = os.listdir(paths[0])
678 698 stylelist = []
679 699 for file in dirlist:
680 700 split = file.split(".")
681 701 if split[-1] in ('orig', 'rej'):
682 702 continue
683 703 if split[0] == "map-cmdline":
684 704 stylelist.append(split[1])
685 705 return ", ".join(sorted(stylelist))
686 706
687 707 def _readmapfile(mapfile):
688 708 """Load template elements from the given map file"""
689 709 if not os.path.exists(mapfile):
690 710 raise error.Abort(_("style '%s' not found") % mapfile,
691 711 hint=_("available styles: %s") % stylelist())
692 712
693 713 base = os.path.dirname(mapfile)
694 714 conf = config.config(includepaths=templatepaths())
695 715 conf.read(mapfile, remap={'': 'templates'})
696 716
697 717 cache = {}
698 718 tmap = {}
699 719 aliases = []
700 720
701 721 val = conf.get('templates', '__base__')
702 722 if val and val[0] not in "'\"":
703 723 # treat as a pointer to a base class for this style
704 724 path = util.normpath(os.path.join(base, val))
705 725
706 726 # fallback check in template paths
707 727 if not os.path.exists(path):
708 728 for p in templatepaths():
709 729 p2 = util.normpath(os.path.join(p, val))
710 730 if os.path.isfile(p2):
711 731 path = p2
712 732 break
713 733 p3 = util.normpath(os.path.join(p2, "map"))
714 734 if os.path.isfile(p3):
715 735 path = p3
716 736 break
717 737
718 738 cache, tmap, aliases = _readmapfile(path)
719 739
720 740 for key, val in conf['templates'].items():
721 741 if not val:
722 742 raise error.ParseError(_('missing value'),
723 743 conf.source('templates', key))
724 744 if val[0] in "'\"":
725 745 if val[0] != val[-1]:
726 746 raise error.ParseError(_('unmatched quotes'),
727 747 conf.source('templates', key))
728 748 cache[key] = unquotestring(val)
729 749 elif key != '__base__':
730 750 val = 'default', val
731 751 if ':' in val[1]:
732 752 val = val[1].split(':', 1)
733 753 tmap[key] = val[0], os.path.join(base, val[1])
734 754 aliases.extend(conf['templatealias'].items())
735 755 return cache, tmap, aliases
736 756
737 757 class templater(object):
738 758
739 759 def __init__(self, filters=None, defaults=None, resources=None,
740 760 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
741 761 """Create template engine optionally with preloaded template fragments
742 762
743 763 - ``filters``: a dict of functions to transform a value into another.
744 764 - ``defaults``: a dict of symbol values/functions; may be overridden
745 765 by a ``mapping`` dict.
746 766 - ``resources``: a resourcemapper object to look up internal data
747 767 (e.g. cache), inaccessible from user template.
748 768 - ``cache``: a dict of preloaded template fragments.
749 769 - ``aliases``: a list of alias (name, replacement) pairs.
750 770
751 771 self.cache may be updated later to register additional template
752 772 fragments.
753 773 """
754 774 if filters is None:
755 775 filters = {}
756 776 if defaults is None:
757 777 defaults = {}
758 778 if cache is None:
759 779 cache = {}
760 780 self.cache = cache.copy()
761 781 self.map = {}
762 782 self.filters = templatefilters.filters.copy()
763 783 self.filters.update(filters)
764 784 self.defaults = defaults
765 785 self._resources = resources
766 786 self._aliases = aliases
767 787 self.minchunk, self.maxchunk = minchunk, maxchunk
768 788 self.ecache = {}
769 789
770 790 @classmethod
771 791 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
772 792 cache=None, minchunk=1024, maxchunk=65536):
773 793 """Create templater from the specified map file"""
774 794 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
775 795 cache, tmap, aliases = _readmapfile(mapfile)
776 796 t.cache.update(cache)
777 797 t.map = tmap
778 798 t._aliases = aliases
779 799 return t
780 800
781 801 def __contains__(self, key):
782 802 return key in self.cache or key in self.map
783 803
784 804 def load(self, t):
785 805 '''Get the template for the given template name. Use a local cache.'''
786 806 if t not in self.cache:
787 807 try:
788 808 self.cache[t] = util.readfile(self.map[t][1])
789 809 except KeyError as inst:
790 810 raise templateutil.TemplateNotFound(
791 811 _('"%s" not in template map') % inst.args[0])
792 812 except IOError as inst:
793 813 reason = (_('template file %s: %s')
794 814 % (self.map[t][1], util.forcebytestr(inst.args[1])))
795 815 raise IOError(inst.args[0], encoding.strfromlocal(reason))
796 816 return self.cache[t]
797 817
798 818 def renderdefault(self, mapping):
799 819 """Render the default unnamed template and return result as string"""
800 820 return self.render('', mapping)
801 821
802 822 def render(self, t, mapping):
803 823 """Render the specified named template and return result as string"""
804 824 return templateutil.stringify(self.generate(t, mapping))
805 825
806 826 def generate(self, t, mapping):
807 827 """Return a generator that renders the specified named template and
808 828 yields chunks"""
809 829 ttype = t in self.map and self.map[t][0] or 'default'
810 830 if ttype not in self.ecache:
811 831 try:
812 832 ecls = engines[ttype]
813 833 except KeyError:
814 834 raise error.Abort(_('invalid template engine: %s') % ttype)
815 835 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
816 836 self._resources, self._aliases)
817 837 proc = self.ecache[ttype]
818 838
819 839 stream = proc.process(t, mapping)
820 840 if self.minchunk:
821 841 stream = util.increasingchunks(stream, min=self.minchunk,
822 842 max=self.maxchunk)
823 843 return stream
824 844
825 845 def templatepaths():
826 846 '''return locations used for template files.'''
827 847 pathsrel = ['templates']
828 848 paths = [os.path.normpath(os.path.join(util.datapath, f))
829 849 for f in pathsrel]
830 850 return [p for p in paths if os.path.isdir(p)]
831 851
832 852 def templatepath(name):
833 853 '''return location of template file. returns None if not found.'''
834 854 for p in templatepaths():
835 855 f = os.path.join(p, name)
836 856 if os.path.exists(f):
837 857 return f
838 858 return None
839 859
840 860 def stylemap(styles, paths=None):
841 861 """Return path to mapfile for a given style.
842 862
843 863 Searches mapfile in the following locations:
844 864 1. templatepath/style/map
845 865 2. templatepath/map-style
846 866 3. templatepath/map
847 867 """
848 868
849 869 if paths is None:
850 870 paths = templatepaths()
851 871 elif isinstance(paths, bytes):
852 872 paths = [paths]
853 873
854 874 if isinstance(styles, bytes):
855 875 styles = [styles]
856 876
857 877 for style in styles:
858 878 # only plain name is allowed to honor template paths
859 879 if (not style
860 880 or style in (pycompat.oscurdir, pycompat.ospardir)
861 881 or pycompat.ossep in style
862 882 or pycompat.osaltsep and pycompat.osaltsep in style):
863 883 continue
864 884 locations = [os.path.join(style, 'map'), 'map-' + style]
865 885 locations.append('map')
866 886
867 887 for path in paths:
868 888 for location in locations:
869 889 mapfile = os.path.join(path, location)
870 890 if os.path.isfile(mapfile):
871 891 return style, mapfile
872 892
873 893 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,163 +1,173
1 1 #require serve
2 2
3 3 #if no-outer-repo
4 4
5 5 no repo
6 6
7 7 $ hg id
8 8 abort: there is no Mercurial repository here (.hg not found)
9 9 [255]
10 10
11 11 #endif
12 12
13 13 create repo
14 14
15 15 $ hg init test
16 16 $ cd test
17 17 $ echo a > a
18 18 $ hg ci -Ama
19 19 adding a
20 20
21 21 basic id usage
22 22
23 23 $ hg id
24 24 cb9a9f314b8b tip
25 25 $ hg id --debug
26 26 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b tip
27 27 $ hg id -q
28 28 cb9a9f314b8b
29 29 $ hg id -v
30 30 cb9a9f314b8b tip
31 31
32 32 with options
33 33
34 34 $ hg id -r.
35 35 cb9a9f314b8b tip
36 36 $ hg id -n
37 37 0
38 38 $ hg id -t
39 39 tip
40 40 $ hg id -b
41 41 default
42 42 $ hg id -i
43 43 cb9a9f314b8b
44 44 $ hg id -n -t -b -i
45 45 cb9a9f314b8b 0 default tip
46 46 $ hg id -Tjson
47 47 [
48 48 {
49 49 "bookmarks": [],
50 50 "branch": "default",
51 51 "dirty": "",
52 52 "id": "cb9a9f314b8b",
53 53 "node": "ffffffffffffffffffffffffffffffffffffffff",
54 54 "parents": [{"node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "rev": 0}],
55 55 "tags": ["tip"]
56 56 }
57 57 ]
58 58
59 59 test template keywords and functions which require changectx:
60 60
61 61 $ hg id -T '{rev} {node|shortest}\n'
62 62 2147483647 ffff
63 63 $ hg id -T '{parents % "{rev} {node|shortest} {desc}\n"}'
64 64 0 cb9a a
65 65
66 test nested template: '{tags}'/'{node}' constants shouldn't override the
67 default keywords, but '{id}' persists because there's no default keyword
68 for '{id}' (issue5612)
69
70 $ hg id -T '{tags}\n'
71 tip
72 $ hg id -T '{revset("null:.") % "{rev}:{node|short} {tags} {id}\n"}'
73 -1:000000000000 cb9a9f314b8b
74 0:cb9a9f314b8b tip cb9a9f314b8b
75
66 76 with modifications
67 77
68 78 $ echo b > a
69 79 $ hg id -n -t -b -i
70 80 cb9a9f314b8b+ 0+ default tip
71 81 $ hg id -Tjson
72 82 [
73 83 {
74 84 "bookmarks": [],
75 85 "branch": "default",
76 86 "dirty": "+",
77 87 "id": "cb9a9f314b8b+",
78 88 "node": "ffffffffffffffffffffffffffffffffffffffff",
79 89 "parents": [{"node": "cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b", "rev": 0}],
80 90 "tags": ["tip"]
81 91 }
82 92 ]
83 93
84 94 other local repo
85 95
86 96 $ cd ..
87 97 $ hg -R test id
88 98 cb9a9f314b8b+ tip
89 99 #if no-outer-repo
90 100 $ hg id test
91 101 cb9a9f314b8b+ tip
92 102 #endif
93 103
94 104 with remote http repo
95 105
96 106 $ cd test
97 107 $ hg serve -p $HGPORT1 -d --pid-file=hg.pid
98 108 $ cat hg.pid >> $DAEMON_PIDS
99 109 $ hg id http://localhost:$HGPORT1/
100 110 cb9a9f314b8b
101 111
102 112 remote with rev number?
103 113
104 114 $ hg id -n http://localhost:$HGPORT1/
105 115 abort: can't query remote revision number, branch, or tags
106 116 [255]
107 117
108 118 remote with tags?
109 119
110 120 $ hg id -t http://localhost:$HGPORT1/
111 121 abort: can't query remote revision number, branch, or tags
112 122 [255]
113 123
114 124 remote with branch?
115 125
116 126 $ hg id -b http://localhost:$HGPORT1/
117 127 abort: can't query remote revision number, branch, or tags
118 128 [255]
119 129
120 130 test bookmark support
121 131
122 132 $ hg bookmark Y
123 133 $ hg bookmark Z
124 134 $ hg bookmarks
125 135 Y 0:cb9a9f314b8b
126 136 * Z 0:cb9a9f314b8b
127 137 $ hg id
128 138 cb9a9f314b8b+ tip Y/Z
129 139 $ hg id --bookmarks
130 140 Y Z
131 141
132 142 test remote identify with bookmarks
133 143
134 144 $ hg id http://localhost:$HGPORT1/
135 145 cb9a9f314b8b Y/Z
136 146 $ hg id --bookmarks http://localhost:$HGPORT1/
137 147 Y Z
138 148 $ hg id -r . http://localhost:$HGPORT1/
139 149 cb9a9f314b8b Y/Z
140 150 $ hg id --bookmarks -r . http://localhost:$HGPORT1/
141 151 Y Z
142 152
143 153 test invalid lookup
144 154
145 155 $ hg id -r noNoNO http://localhost:$HGPORT1/
146 156 abort: unknown revision 'noNoNO'!
147 157 [255]
148 158
149 159 Make sure we do not obscure unknown requires file entries (issue2649)
150 160
151 161 $ echo fake >> .hg/requires
152 162 $ hg id
153 163 abort: repository requires features unknown to this Mercurial: fake!
154 164 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
155 165 [255]
156 166
157 167 $ cd ..
158 168 #if no-outer-repo
159 169 $ hg id test
160 170 abort: repository requires features unknown to this Mercurial: fake!
161 171 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
162 172 [255]
163 173 #endif
General Comments 0
You need to be logged in to leave comments. Login now