##// END OF EJS Templates
formatter: proxy fm.context() through converter...
Yuya Nishihara -
r33090:04b3743c default
parent child Browse files
Show More
@@ -1,523 +1,530 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.fout = sys.stdout # redirect to doctest
49 49 ... ui.verbose = verbose
50 50 ... return fn(ui, ui.formatter(fn.__name__, opts))
51 51
52 52 Basic example:
53 53
54 54 >>> def files(ui, fm):
55 55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
56 56 ... for f in files:
57 57 ... fm.startitem()
58 58 ... fm.write('path', '%s', f[0])
59 59 ... fm.condwrite(ui.verbose, 'date', ' %s',
60 60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
61 61 ... fm.data(size=f[1])
62 62 ... fm.plain('\\n')
63 63 ... fm.end()
64 64 >>> show(files)
65 65 foo
66 66 bar
67 67 >>> show(files, verbose=True)
68 68 foo 1970-01-01 00:00:00
69 69 bar 1970-01-01 00:00:01
70 70 >>> show(files, template='json')
71 71 [
72 72 {
73 73 "date": [0, 0],
74 74 "path": "foo",
75 75 "size": 123
76 76 },
77 77 {
78 78 "date": [1, 0],
79 79 "path": "bar",
80 80 "size": 456
81 81 }
82 82 ]
83 83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
84 84 path: foo
85 85 date: 1970-01-01T00:00:00+00:00
86 86 path: bar
87 87 date: 1970-01-01T00:00:01+00:00
88 88
89 89 Nested example:
90 90
91 91 >>> def subrepos(ui, fm):
92 92 ... fm.startitem()
93 93 ... fm.write('repo', '[%s]\\n', 'baz')
94 94 ... files(ui, fm.nested('files'))
95 95 ... fm.end()
96 96 >>> show(subrepos)
97 97 [baz]
98 98 foo
99 99 bar
100 100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
101 101 baz: foo, bar
102 102 """
103 103
104 104 from __future__ import absolute_import
105 105
106 106 import collections
107 107 import contextlib
108 108 import itertools
109 109 import os
110 110
111 111 from .i18n import _
112 112 from .node import (
113 113 hex,
114 114 short,
115 115 )
116 116
117 117 from . import (
118 118 error,
119 119 pycompat,
120 120 templatefilters,
121 121 templatekw,
122 122 templater,
123 123 util,
124 124 )
125 125
126 126 pickle = util.pickle
127 127
128 128 class _nullconverter(object):
129 129 '''convert non-primitive data types to be processed by formatter'''
130
131 # set to True if context object should be stored as item
132 storecontext = False
133
130 134 @staticmethod
131 135 def formatdate(date, fmt):
132 136 '''convert date tuple to appropriate format'''
133 137 return date
134 138 @staticmethod
135 139 def formatdict(data, key, value, fmt, sep):
136 140 '''convert dict or key-value pairs to appropriate dict format'''
137 141 # use plain dict instead of util.sortdict so that data can be
138 142 # serialized as a builtin dict in pickle output
139 143 return dict(data)
140 144 @staticmethod
141 145 def formatlist(data, name, fmt, sep):
142 146 '''convert iterable to appropriate list format'''
143 147 return list(data)
144 148
145 149 class baseformatter(object):
146 150 def __init__(self, ui, topic, opts, converter):
147 151 self._ui = ui
148 152 self._topic = topic
149 153 self._style = opts.get("style")
150 154 self._template = opts.get("template")
151 155 self._converter = converter
152 156 self._item = None
153 157 # function to convert node to string suitable for this output
154 158 self.hexfunc = hex
155 159 def __enter__(self):
156 160 return self
157 161 def __exit__(self, exctype, excvalue, traceback):
158 162 if exctype is None:
159 163 self.end()
160 164 def _showitem(self):
161 165 '''show a formatted item once all data is collected'''
162 166 pass
163 167 def startitem(self):
164 168 '''begin an item in the format list'''
165 169 if self._item is not None:
166 170 self._showitem()
167 171 self._item = {}
168 172 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
169 173 '''convert date tuple to appropriate format'''
170 174 return self._converter.formatdate(date, fmt)
171 175 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
172 176 '''convert dict or key-value pairs to appropriate dict format'''
173 177 return self._converter.formatdict(data, key, value, fmt, sep)
174 178 def formatlist(self, data, name, fmt='%s', sep=' '):
175 179 '''convert iterable to appropriate list format'''
176 180 # name is mandatory argument for now, but it could be optional if
177 181 # we have default template keyword, e.g. {item}
178 182 return self._converter.formatlist(data, name, fmt, sep)
179 183 def context(self, **ctxs):
180 184 '''insert context objects to be used to render template keywords'''
181 pass
185 ctxs = pycompat.byteskwargs(ctxs)
186 assert all(k == 'ctx' for k in ctxs)
187 if self._converter.storecontext:
188 self._item.update(ctxs)
182 189 def data(self, **data):
183 190 '''insert data into item that's not shown in default output'''
184 191 data = pycompat.byteskwargs(data)
185 192 self._item.update(data)
186 193 def write(self, fields, deftext, *fielddata, **opts):
187 194 '''do default text output while assigning data to item'''
188 195 fieldkeys = fields.split()
189 196 assert len(fieldkeys) == len(fielddata)
190 197 self._item.update(zip(fieldkeys, fielddata))
191 198 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
192 199 '''do conditional write (primarily for plain formatter)'''
193 200 fieldkeys = fields.split()
194 201 assert len(fieldkeys) == len(fielddata)
195 202 self._item.update(zip(fieldkeys, fielddata))
196 203 def plain(self, text, **opts):
197 204 '''show raw text for non-templated mode'''
198 205 pass
199 206 def isplain(self):
200 207 '''check for plain formatter usage'''
201 208 return False
202 209 def nested(self, field):
203 210 '''sub formatter to store nested data in the specified field'''
204 211 self._item[field] = data = []
205 212 return _nestedformatter(self._ui, self._converter, data)
206 213 def end(self):
207 214 '''end output for the formatter'''
208 215 if self._item is not None:
209 216 self._showitem()
210 217
211 218 def nullformatter(ui, topic):
212 219 '''formatter that prints nothing'''
213 220 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
214 221
215 222 class _nestedformatter(baseformatter):
216 223 '''build sub items and store them in the parent formatter'''
217 224 def __init__(self, ui, converter, data):
218 225 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
219 226 self._data = data
220 227 def _showitem(self):
221 228 self._data.append(self._item)
222 229
223 230 def _iteritems(data):
224 231 '''iterate key-value pairs in stable order'''
225 232 if isinstance(data, dict):
226 233 return sorted(data.iteritems())
227 234 return data
228 235
229 236 class _plainconverter(object):
230 237 '''convert non-primitive data types to text'''
238
239 storecontext = False
240
231 241 @staticmethod
232 242 def formatdate(date, fmt):
233 243 '''stringify date tuple in the given format'''
234 244 return util.datestr(date, fmt)
235 245 @staticmethod
236 246 def formatdict(data, key, value, fmt, sep):
237 247 '''stringify key-value pairs separated by sep'''
238 248 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
239 249 @staticmethod
240 250 def formatlist(data, name, fmt, sep):
241 251 '''stringify iterable separated by sep'''
242 252 return sep.join(fmt % e for e in data)
243 253
244 254 class plainformatter(baseformatter):
245 255 '''the default text output scheme'''
246 256 def __init__(self, ui, out, topic, opts):
247 257 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
248 258 if ui.debugflag:
249 259 self.hexfunc = hex
250 260 else:
251 261 self.hexfunc = short
252 262 if ui is out:
253 263 self._write = ui.write
254 264 else:
255 265 self._write = lambda s, **opts: out.write(s)
256 266 def startitem(self):
257 267 pass
258 268 def data(self, **data):
259 269 pass
260 270 def write(self, fields, deftext, *fielddata, **opts):
261 271 self._write(deftext % fielddata, **opts)
262 272 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
263 273 '''do conditional write'''
264 274 if cond:
265 275 self._write(deftext % fielddata, **opts)
266 276 def plain(self, text, **opts):
267 277 self._write(text, **opts)
268 278 def isplain(self):
269 279 return True
270 280 def nested(self, field):
271 281 # nested data will be directly written to ui
272 282 return self
273 283 def end(self):
274 284 pass
275 285
276 286 class debugformatter(baseformatter):
277 287 def __init__(self, ui, out, topic, opts):
278 288 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
279 289 self._out = out
280 290 self._out.write("%s = [\n" % self._topic)
281 291 def _showitem(self):
282 292 self._out.write(" " + repr(self._item) + ",\n")
283 293 def end(self):
284 294 baseformatter.end(self)
285 295 self._out.write("]\n")
286 296
287 297 class pickleformatter(baseformatter):
288 298 def __init__(self, ui, out, topic, opts):
289 299 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
290 300 self._out = out
291 301 self._data = []
292 302 def _showitem(self):
293 303 self._data.append(self._item)
294 304 def end(self):
295 305 baseformatter.end(self)
296 306 self._out.write(pickle.dumps(self._data))
297 307
298 308 class jsonformatter(baseformatter):
299 309 def __init__(self, ui, out, topic, opts):
300 310 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
301 311 self._out = out
302 312 self._out.write("[")
303 313 self._first = True
304 314 def _showitem(self):
305 315 if self._first:
306 316 self._first = False
307 317 else:
308 318 self._out.write(",")
309 319
310 320 self._out.write("\n {\n")
311 321 first = True
312 322 for k, v in sorted(self._item.items()):
313 323 if first:
314 324 first = False
315 325 else:
316 326 self._out.write(",\n")
317 327 u = templatefilters.json(v, paranoid=False)
318 328 self._out.write(' "%s": %s' % (k, u))
319 329 self._out.write("\n }")
320 330 def end(self):
321 331 baseformatter.end(self)
322 332 self._out.write("\n]\n")
323 333
324 334 class _templateconverter(object):
325 335 '''convert non-primitive data types to be processed by templater'''
336
337 storecontext = True
338
326 339 @staticmethod
327 340 def formatdate(date, fmt):
328 341 '''return date tuple'''
329 342 return date
330 343 @staticmethod
331 344 def formatdict(data, key, value, fmt, sep):
332 345 '''build object that can be evaluated as either plain string or dict'''
333 346 data = util.sortdict(_iteritems(data))
334 347 def f():
335 348 yield _plainconverter.formatdict(data, key, value, fmt, sep)
336 349 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt,
337 350 gen=f())
338 351 @staticmethod
339 352 def formatlist(data, name, fmt, sep):
340 353 '''build object that can be evaluated as either plain string or list'''
341 354 data = list(data)
342 355 def f():
343 356 yield _plainconverter.formatlist(data, name, fmt, sep)
344 357 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f())
345 358
346 359 class templateformatter(baseformatter):
347 360 def __init__(self, ui, out, topic, opts):
348 361 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
349 362 self._out = out
350 363 spec = lookuptemplate(ui, topic, opts.get('template', ''))
351 364 self._tref = spec.ref
352 365 self._t = loadtemplater(ui, spec, cache=templatekw.defaulttempl)
353 366 self._parts = templatepartsmap(spec, self._t,
354 367 ['docheader', 'docfooter', 'separator'])
355 368 self._counter = itertools.count()
356 369 self._cache = {} # for templatekw/funcs to store reusable data
357 370 self._renderitem('docheader', {})
358 371
359 def context(self, **ctxs):
360 '''insert context objects to be used to render template keywords'''
361 ctxs = pycompat.byteskwargs(ctxs)
362 assert all(k == 'ctx' for k in ctxs)
363 self._item.update(ctxs)
364
365 372 def _showitem(self):
366 373 item = self._item.copy()
367 374 item['index'] = index = next(self._counter)
368 375 if index > 0:
369 376 self._renderitem('separator', {})
370 377 self._renderitem(self._tref, item)
371 378
372 379 def _renderitem(self, part, item):
373 380 if part not in self._parts:
374 381 return
375 382 ref = self._parts[part]
376 383
377 384 # TODO: add support for filectx. probably each template keyword or
378 385 # function will have to declare dependent resources. e.g.
379 386 # @templatekeyword(..., requires=('ctx',))
380 387 props = {}
381 388 if 'ctx' in item:
382 389 props.update(templatekw.keywords)
383 390 # explicitly-defined fields precede templatekw
384 391 props.update(item)
385 392 if 'ctx' in item:
386 393 # but template resources must be always available
387 394 props['templ'] = self._t
388 395 props['repo'] = props['ctx'].repo()
389 396 props['revcache'] = {}
390 397 props = pycompat.strkwargs(props)
391 398 g = self._t(ref, ui=self._ui, cache=self._cache, **props)
392 399 self._out.write(templater.stringify(g))
393 400
394 401 def end(self):
395 402 baseformatter.end(self)
396 403 self._renderitem('docfooter', {})
397 404
398 405 templatespec = collections.namedtuple(r'templatespec',
399 406 r'ref tmpl mapfile')
400 407
401 408 def lookuptemplate(ui, topic, tmpl):
402 409 """Find the template matching the given -T/--template spec 'tmpl'
403 410
404 411 'tmpl' can be any of the following:
405 412
406 413 - a literal template (e.g. '{rev}')
407 414 - a map-file name or path (e.g. 'changelog')
408 415 - a reference to [templates] in config file
409 416 - a path to raw template file
410 417
411 418 A map file defines a stand-alone template environment. If a map file
412 419 selected, all templates defined in the file will be loaded, and the
413 420 template matching the given topic will be rendered. No aliases will be
414 421 loaded from user config.
415 422
416 423 If no map file selected, all templates in [templates] section will be
417 424 available as well as aliases in [templatealias].
418 425 """
419 426
420 427 # looks like a literal template?
421 428 if '{' in tmpl:
422 429 return templatespec('', tmpl, None)
423 430
424 431 # perhaps a stock style?
425 432 if not os.path.split(tmpl)[0]:
426 433 mapname = (templater.templatepath('map-cmdline.' + tmpl)
427 434 or templater.templatepath(tmpl))
428 435 if mapname and os.path.isfile(mapname):
429 436 return templatespec(topic, None, mapname)
430 437
431 438 # perhaps it's a reference to [templates]
432 439 if ui.config('templates', tmpl):
433 440 return templatespec(tmpl, None, None)
434 441
435 442 if tmpl == 'list':
436 443 ui.write(_("available styles: %s\n") % templater.stylelist())
437 444 raise error.Abort(_("specify a template"))
438 445
439 446 # perhaps it's a path to a map or a template
440 447 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
441 448 # is it a mapfile for a style?
442 449 if os.path.basename(tmpl).startswith("map-"):
443 450 return templatespec(topic, None, os.path.realpath(tmpl))
444 451 with util.posixfile(tmpl, 'rb') as f:
445 452 tmpl = f.read()
446 453 return templatespec('', tmpl, None)
447 454
448 455 # constant string?
449 456 return templatespec('', tmpl, None)
450 457
451 458 def templatepartsmap(spec, t, partnames):
452 459 """Create a mapping of {part: ref}"""
453 460 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
454 461 if spec.mapfile:
455 462 partsmap.update((p, p) for p in partnames if p in t)
456 463 elif spec.ref:
457 464 for part in partnames:
458 465 ref = '%s:%s' % (spec.ref, part) # select config sub-section
459 466 if ref in t:
460 467 partsmap[part] = ref
461 468 return partsmap
462 469
463 470 def loadtemplater(ui, spec, cache=None):
464 471 """Create a templater from either a literal template or loading from
465 472 a map file"""
466 473 assert not (spec.tmpl and spec.mapfile)
467 474 if spec.mapfile:
468 475 return templater.templater.frommapfile(spec.mapfile, cache=cache)
469 476 return maketemplater(ui, spec.tmpl, cache=cache)
470 477
471 478 def maketemplater(ui, tmpl, cache=None):
472 479 """Create a templater from a string template 'tmpl'"""
473 480 aliases = ui.configitems('templatealias')
474 481 t = templater.templater(cache=cache, aliases=aliases)
475 482 t.cache.update((k, templater.unquotestring(v))
476 483 for k, v in ui.configitems('templates'))
477 484 if tmpl:
478 485 t.cache[''] = tmpl
479 486 return t
480 487
481 488 def formatter(ui, out, topic, opts):
482 489 template = opts.get("template", "")
483 490 if template == "json":
484 491 return jsonformatter(ui, out, topic, opts)
485 492 elif template == "pickle":
486 493 return pickleformatter(ui, out, topic, opts)
487 494 elif template == "debug":
488 495 return debugformatter(ui, out, topic, opts)
489 496 elif template != "":
490 497 return templateformatter(ui, out, topic, opts)
491 498 # developer config: ui.formatdebug
492 499 elif ui.configbool('ui', 'formatdebug'):
493 500 return debugformatter(ui, out, topic, opts)
494 501 # deprecated config: ui.formatjson
495 502 elif ui.configbool('ui', 'formatjson'):
496 503 return jsonformatter(ui, out, topic, opts)
497 504 return plainformatter(ui, out, topic, opts)
498 505
499 506 @contextlib.contextmanager
500 507 def openformatter(ui, filename, topic, opts):
501 508 """Create a formatter that writes outputs to the specified file
502 509
503 510 Must be invoked using the 'with' statement.
504 511 """
505 512 with util.posixfile(filename, 'wb') as out:
506 513 with formatter(ui, out, topic, opts) as fm:
507 514 yield fm
508 515
509 516 @contextlib.contextmanager
510 517 def _neverending(fm):
511 518 yield fm
512 519
513 520 def maybereopen(fm, filename, opts):
514 521 """Create a formatter backed by file if filename specified, else return
515 522 the given formatter
516 523
517 524 Must be invoked using the 'with' statement. This will never call fm.end()
518 525 of the given formatter.
519 526 """
520 527 if filename:
521 528 return openformatter(fm._ui, filename, fm._topic, opts)
522 529 else:
523 530 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now