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