##// END OF EJS Templates
formatter: unblock storing fctx as a template resource...
Yuya Nishihara -
r36999:939e0983 default
parent child Browse files
Show More
@@ -1,566 +1,579 b''
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 assert all(k == 'ctx' for k in ctxs)
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 # TODO: add support for filectx
399 398 props = {}
400 399 # explicitly-defined fields precede templatekw
401 400 props.update(item)
402 if 'ctx' in item:
401 if 'ctx' in item or 'fctx' in item:
403 402 # but template resources must be always available
404 props['repo'] = props['ctx'].repo()
405 403 props['revcache'] = {}
406 404 props = pycompat.strkwargs(props)
407 405 g = self._t(ref, **props)
408 406 self._out.write(templateutil.stringify(g))
409 407
410 408 def end(self):
411 409 baseformatter.end(self)
412 410 self._renderitem('docfooter', {})
413 411
414 412 templatespec = collections.namedtuple(r'templatespec',
415 413 r'ref tmpl mapfile')
416 414
417 415 def lookuptemplate(ui, topic, tmpl):
418 416 """Find the template matching the given -T/--template spec 'tmpl'
419 417
420 418 'tmpl' can be any of the following:
421 419
422 420 - a literal template (e.g. '{rev}')
423 421 - a map-file name or path (e.g. 'changelog')
424 422 - a reference to [templates] in config file
425 423 - a path to raw template file
426 424
427 425 A map file defines a stand-alone template environment. If a map file
428 426 selected, all templates defined in the file will be loaded, and the
429 427 template matching the given topic will be rendered. Aliases won't be
430 428 loaded from user config, but from the map file.
431 429
432 430 If no map file selected, all templates in [templates] section will be
433 431 available as well as aliases in [templatealias].
434 432 """
435 433
436 434 # looks like a literal template?
437 435 if '{' in tmpl:
438 436 return templatespec('', tmpl, None)
439 437
440 438 # perhaps a stock style?
441 439 if not os.path.split(tmpl)[0]:
442 440 mapname = (templater.templatepath('map-cmdline.' + tmpl)
443 441 or templater.templatepath(tmpl))
444 442 if mapname and os.path.isfile(mapname):
445 443 return templatespec(topic, None, mapname)
446 444
447 445 # perhaps it's a reference to [templates]
448 446 if ui.config('templates', tmpl):
449 447 return templatespec(tmpl, None, None)
450 448
451 449 if tmpl == 'list':
452 450 ui.write(_("available styles: %s\n") % templater.stylelist())
453 451 raise error.Abort(_("specify a template"))
454 452
455 453 # perhaps it's a path to a map or a template
456 454 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
457 455 # is it a mapfile for a style?
458 456 if os.path.basename(tmpl).startswith("map-"):
459 457 return templatespec(topic, None, os.path.realpath(tmpl))
460 458 with util.posixfile(tmpl, 'rb') as f:
461 459 tmpl = f.read()
462 460 return templatespec('', tmpl, None)
463 461
464 462 # constant string?
465 463 return templatespec('', tmpl, None)
466 464
467 465 def templatepartsmap(spec, t, partnames):
468 466 """Create a mapping of {part: ref}"""
469 467 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
470 468 if spec.mapfile:
471 469 partsmap.update((p, p) for p in partnames if p in t)
472 470 elif spec.ref:
473 471 for part in partnames:
474 472 ref = '%s:%s' % (spec.ref, part) # select config sub-section
475 473 if ref in t:
476 474 partsmap[part] = ref
477 475 return partsmap
478 476
479 477 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
480 478 """Create a templater from either a literal template or loading from
481 479 a map file"""
482 480 assert not (spec.tmpl and spec.mapfile)
483 481 if spec.mapfile:
484 482 frommapfile = templater.templater.frommapfile
485 483 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
486 484 cache=cache)
487 485 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
488 486 cache=cache)
489 487
490 488 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
491 489 """Create a templater from a string template 'tmpl'"""
492 490 aliases = ui.configitems('templatealias')
493 491 t = templater.templater(defaults=defaults, resources=resources,
494 492 cache=cache, aliases=aliases)
495 493 t.cache.update((k, templater.unquotestring(v))
496 494 for k, v in ui.configitems('templates'))
497 495 if tmpl:
498 496 t.cache[''] = tmpl
499 497 return t
500 498
501 499 def templateresources(ui, repo=None):
502 500 """Create a dict of template resources designed for the default templatekw
503 501 and function"""
504 502 resmap = {
505 503 'cache': {}, # for templatekw/funcs to store reusable data
506 504 'repo': repo,
507 505 'ui': ui,
508 506 }
509 507
510 508 def getsome(context, mapping, key):
511 509 v = mapping.get(key)
512 510 if v is not None:
513 511 return v
514 512 return resmap.get(key)
515 513
514 def getctx(context, mapping, key):
515 ctx = mapping.get('ctx')
516 if ctx is not None:
517 return ctx
518 fctx = mapping.get('fctx')
519 if fctx is not None:
520 return fctx.changectx()
521
522 def getrepo(context, mapping, key):
523 ctx = getctx(context, mapping, 'ctx')
524 if ctx is not None:
525 return ctx.repo()
526 return getsome(context, mapping, key)
527
516 528 return {
517 529 'cache': getsome,
518 'ctx': getsome,
519 'repo': getsome,
530 'ctx': getctx,
531 'fctx': getsome,
532 'repo': getrepo,
520 533 'revcache': getsome, # per-ctx cache; set later
521 534 'ui': getsome,
522 535 }
523 536
524 537 def formatter(ui, out, topic, opts):
525 538 template = opts.get("template", "")
526 539 if template == "json":
527 540 return jsonformatter(ui, out, topic, opts)
528 541 elif template == "pickle":
529 542 return pickleformatter(ui, out, topic, opts)
530 543 elif template == "debug":
531 544 return debugformatter(ui, out, topic, opts)
532 545 elif template != "":
533 546 return templateformatter(ui, out, topic, opts)
534 547 # developer config: ui.formatdebug
535 548 elif ui.configbool('ui', 'formatdebug'):
536 549 return debugformatter(ui, out, topic, opts)
537 550 # deprecated config: ui.formatjson
538 551 elif ui.configbool('ui', 'formatjson'):
539 552 return jsonformatter(ui, out, topic, opts)
540 553 return plainformatter(ui, out, topic, opts)
541 554
542 555 @contextlib.contextmanager
543 556 def openformatter(ui, filename, topic, opts):
544 557 """Create a formatter that writes outputs to the specified file
545 558
546 559 Must be invoked using the 'with' statement.
547 560 """
548 561 with util.posixfile(filename, 'wb') as out:
549 562 with formatter(ui, out, topic, opts) as fm:
550 563 yield fm
551 564
552 565 @contextlib.contextmanager
553 566 def _neverending(fm):
554 567 yield fm
555 568
556 569 def maybereopen(fm, filename, opts):
557 570 """Create a formatter backed by file if filename specified, else return
558 571 the given formatter
559 572
560 573 Must be invoked using the 'with' statement. This will never call fm.end()
561 574 of the given formatter.
562 575 """
563 576 if filename:
564 577 return openformatter(fm._ui, filename, fm._topic, opts)
565 578 else:
566 579 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now