##// END OF EJS Templates
formatter: always store a literal template unnamed...
Yuya Nishihara -
r32876:8da65da0 default
parent child Browse files
Show More
@@ -1,493 +1,490 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 assert all(k == 'ctx' for k in ctxs)
358 358 self._item.update(ctxs)
359 359 def _showitem(self):
360 360 # TODO: add support for filectx. probably each template keyword or
361 361 # function will have to declare dependent resources. e.g.
362 362 # @templatekeyword(..., requires=('ctx',))
363 363 props = {}
364 364 if 'ctx' in self._item:
365 365 props.update(templatekw.keywords)
366 366 props['index'] = next(self._counter)
367 367 # explicitly-defined fields precede templatekw
368 368 props.update(self._item)
369 369 if 'ctx' in self._item:
370 370 # but template resources must be always available
371 371 props['templ'] = self._t
372 372 props['repo'] = props['ctx'].repo()
373 373 props['revcache'] = {}
374 374 g = self._t(self._tref, ui=self._ui, cache=self._cache, **props)
375 375 self._out.write(templater.stringify(g))
376 376
377 377 templatespec = collections.namedtuple(r'templatespec',
378 378 r'ref tmpl mapfile')
379 379
380 380 def lookuptemplate(ui, topic, tmpl):
381 381 """Find the template matching the given -T/--template spec 'tmpl'
382 382
383 383 'tmpl' can be any of the following:
384 384
385 385 - a literal template (e.g. '{rev}')
386 386 - a map-file name or path (e.g. 'changelog')
387 387 - a reference to [templates] in config file
388 388 - a path to raw template file
389 389
390 390 A map file defines a stand-alone template environment. If a map file
391 391 selected, all templates defined in the file will be loaded, and the
392 392 template matching the given topic will be rendered. No aliases will be
393 393 loaded from user config.
394 394
395 395 If no map file selected, all templates in [templates] section will be
396 396 available as well as aliases in [templatealias].
397 397 """
398 398
399 399 # looks like a literal template?
400 400 if '{' in tmpl:
401 401 return templatespec('', tmpl, None)
402 402
403 403 # perhaps a stock style?
404 404 if not os.path.split(tmpl)[0]:
405 405 mapname = (templater.templatepath('map-cmdline.' + tmpl)
406 406 or templater.templatepath(tmpl))
407 407 if mapname and os.path.isfile(mapname):
408 408 return templatespec(topic, None, mapname)
409 409
410 410 # perhaps it's a reference to [templates]
411 411 if ui.config('templates', tmpl):
412 412 return templatespec(tmpl, None, None)
413 413
414 414 if tmpl == 'list':
415 415 ui.write(_("available styles: %s\n") % templater.stylelist())
416 416 raise error.Abort(_("specify a template"))
417 417
418 418 # perhaps it's a path to a map or a template
419 419 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
420 420 # is it a mapfile for a style?
421 421 if os.path.basename(tmpl).startswith("map-"):
422 422 return templatespec(topic, None, os.path.realpath(tmpl))
423 423 with util.posixfile(tmpl, 'rb') as f:
424 424 tmpl = f.read()
425 425 return templatespec('', tmpl, None)
426 426
427 427 # constant string?
428 428 return templatespec('', tmpl, None)
429 429
430 430 def loadtemplater(ui, spec, cache=None):
431 431 """Create a templater from either a literal template or loading from
432 432 a map file"""
433 433 assert not (spec.tmpl and spec.mapfile)
434 434 if spec.mapfile:
435 435 return templater.templater.frommapfile(spec.mapfile, cache=cache)
436 return _maketemplater(ui, spec.ref, spec.tmpl, cache=cache)
436 return maketemplater(ui, spec.tmpl, cache=cache)
437 437
438 438 def maketemplater(ui, tmpl, cache=None):
439 439 """Create a templater from a string template 'tmpl'"""
440 return _maketemplater(ui, '', tmpl, cache=cache)
441
442 def _maketemplater(ui, topic, tmpl, cache=None):
443 440 aliases = ui.configitems('templatealias')
444 441 t = templater.templater(cache=cache, aliases=aliases)
445 442 t.cache.update((k, templater.unquotestring(v))
446 443 for k, v in ui.configitems('templates'))
447 444 if tmpl:
448 t.cache[topic] = tmpl
445 t.cache[''] = tmpl
449 446 return t
450 447
451 448 def formatter(ui, out, topic, opts):
452 449 template = opts.get("template", "")
453 450 if template == "json":
454 451 return jsonformatter(ui, out, topic, opts)
455 452 elif template == "pickle":
456 453 return pickleformatter(ui, out, topic, opts)
457 454 elif template == "debug":
458 455 return debugformatter(ui, out, topic, opts)
459 456 elif template != "":
460 457 return templateformatter(ui, out, topic, opts)
461 458 # developer config: ui.formatdebug
462 459 elif ui.configbool('ui', 'formatdebug'):
463 460 return debugformatter(ui, out, topic, opts)
464 461 # deprecated config: ui.formatjson
465 462 elif ui.configbool('ui', 'formatjson'):
466 463 return jsonformatter(ui, out, topic, opts)
467 464 return plainformatter(ui, out, topic, opts)
468 465
469 466 @contextlib.contextmanager
470 467 def openformatter(ui, filename, topic, opts):
471 468 """Create a formatter that writes outputs to the specified file
472 469
473 470 Must be invoked using the 'with' statement.
474 471 """
475 472 with util.posixfile(filename, 'wb') as out:
476 473 with formatter(ui, out, topic, opts) as fm:
477 474 yield fm
478 475
479 476 @contextlib.contextmanager
480 477 def _neverending(fm):
481 478 yield fm
482 479
483 480 def maybereopen(fm, filename, opts):
484 481 """Create a formatter backed by file if filename specified, else return
485 482 the given formatter
486 483
487 484 Must be invoked using the 'with' statement. This will never call fm.end()
488 485 of the given formatter.
489 486 """
490 487 if filename:
491 488 return openformatter(fm._ui, filename, fm._topic, opts)
492 489 else:
493 490 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now