##// END OF EJS Templates
formatter: add argument to change output file of non-plain formatter...
Yuya Nishihara -
r31182:5660c45e default
parent child Browse files
Show More
@@ -1,439 +1,443
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 os
107 107
108 108 from .i18n import _
109 109 from .node import (
110 110 hex,
111 111 short,
112 112 )
113 113
114 114 from . import (
115 115 encoding,
116 116 error,
117 117 templatekw,
118 118 templater,
119 119 util,
120 120 )
121 121
122 122 pickle = util.pickle
123 123
124 124 class _nullconverter(object):
125 125 '''convert non-primitive data types to be processed by formatter'''
126 126 @staticmethod
127 127 def formatdate(date, fmt):
128 128 '''convert date tuple to appropriate format'''
129 129 return date
130 130 @staticmethod
131 131 def formatdict(data, key, value, fmt, sep):
132 132 '''convert dict or key-value pairs to appropriate dict format'''
133 133 # use plain dict instead of util.sortdict so that data can be
134 134 # serialized as a builtin dict in pickle output
135 135 return dict(data)
136 136 @staticmethod
137 137 def formatlist(data, name, fmt, sep):
138 138 '''convert iterable to appropriate list format'''
139 139 return list(data)
140 140
141 141 class baseformatter(object):
142 142 def __init__(self, ui, topic, opts, converter):
143 143 self._ui = ui
144 144 self._topic = topic
145 145 self._style = opts.get("style")
146 146 self._template = opts.get("template")
147 147 self._converter = converter
148 148 self._item = None
149 149 # function to convert node to string suitable for this output
150 150 self.hexfunc = hex
151 151 def __enter__(self):
152 152 return self
153 153 def __exit__(self, exctype, excvalue, traceback):
154 154 if exctype is None:
155 155 self.end()
156 156 def _showitem(self):
157 157 '''show a formatted item once all data is collected'''
158 158 pass
159 159 def startitem(self):
160 160 '''begin an item in the format list'''
161 161 if self._item is not None:
162 162 self._showitem()
163 163 self._item = {}
164 164 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
165 165 '''convert date tuple to appropriate format'''
166 166 return self._converter.formatdate(date, fmt)
167 167 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
168 168 '''convert dict or key-value pairs to appropriate dict format'''
169 169 return self._converter.formatdict(data, key, value, fmt, sep)
170 170 def formatlist(self, data, name, fmt='%s', sep=' '):
171 171 '''convert iterable to appropriate list format'''
172 172 # name is mandatory argument for now, but it could be optional if
173 173 # we have default template keyword, e.g. {item}
174 174 return self._converter.formatlist(data, name, fmt, sep)
175 175 def context(self, **ctxs):
176 176 '''insert context objects to be used to render template keywords'''
177 177 pass
178 178 def data(self, **data):
179 179 '''insert data into item that's not shown in default output'''
180 180 self._item.update(data)
181 181 def write(self, fields, deftext, *fielddata, **opts):
182 182 '''do default text output while assigning data to item'''
183 183 fieldkeys = fields.split()
184 184 assert len(fieldkeys) == len(fielddata)
185 185 self._item.update(zip(fieldkeys, fielddata))
186 186 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
187 187 '''do conditional write (primarily for plain formatter)'''
188 188 fieldkeys = fields.split()
189 189 assert len(fieldkeys) == len(fielddata)
190 190 self._item.update(zip(fieldkeys, fielddata))
191 191 def plain(self, text, **opts):
192 192 '''show raw text for non-templated mode'''
193 193 pass
194 194 def isplain(self):
195 195 '''check for plain formatter usage'''
196 196 return False
197 197 def nested(self, field):
198 198 '''sub formatter to store nested data in the specified field'''
199 199 self._item[field] = data = []
200 200 return _nestedformatter(self._ui, self._converter, data)
201 201 def end(self):
202 202 '''end output for the formatter'''
203 203 if self._item is not None:
204 204 self._showitem()
205 205
206 206 class _nestedformatter(baseformatter):
207 207 '''build sub items and store them in the parent formatter'''
208 208 def __init__(self, ui, converter, data):
209 209 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
210 210 self._data = data
211 211 def _showitem(self):
212 212 self._data.append(self._item)
213 213
214 214 def _iteritems(data):
215 215 '''iterate key-value pairs in stable order'''
216 216 if isinstance(data, dict):
217 217 return sorted(data.iteritems())
218 218 return data
219 219
220 220 class _plainconverter(object):
221 221 '''convert non-primitive data types to text'''
222 222 @staticmethod
223 223 def formatdate(date, fmt):
224 224 '''stringify date tuple in the given format'''
225 225 return util.datestr(date, fmt)
226 226 @staticmethod
227 227 def formatdict(data, key, value, fmt, sep):
228 228 '''stringify key-value pairs separated by sep'''
229 229 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
230 230 @staticmethod
231 231 def formatlist(data, name, fmt, sep):
232 232 '''stringify iterable separated by sep'''
233 233 return sep.join(fmt % e for e in data)
234 234
235 235 class plainformatter(baseformatter):
236 236 '''the default text output scheme'''
237 237 def __init__(self, ui, topic, opts):
238 238 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
239 239 if ui.debugflag:
240 240 self.hexfunc = hex
241 241 else:
242 242 self.hexfunc = short
243 243 def startitem(self):
244 244 pass
245 245 def data(self, **data):
246 246 pass
247 247 def write(self, fields, deftext, *fielddata, **opts):
248 248 self._ui.write(deftext % fielddata, **opts)
249 249 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
250 250 '''do conditional write'''
251 251 if cond:
252 252 self._ui.write(deftext % fielddata, **opts)
253 253 def plain(self, text, **opts):
254 254 self._ui.write(text, **opts)
255 255 def isplain(self):
256 256 return True
257 257 def nested(self, field):
258 258 # nested data will be directly written to ui
259 259 return self
260 260 def end(self):
261 261 pass
262 262
263 263 class debugformatter(baseformatter):
264 def __init__(self, ui, topic, opts):
264 def __init__(self, ui, out, topic, opts):
265 265 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
266 self._ui.write("%s = [\n" % self._topic)
266 self._out = out
267 self._out.write("%s = [\n" % self._topic)
267 268 def _showitem(self):
268 self._ui.write(" " + repr(self._item) + ",\n")
269 self._out.write(" " + repr(self._item) + ",\n")
269 270 def end(self):
270 271 baseformatter.end(self)
271 self._ui.write("]\n")
272 self._out.write("]\n")
272 273
273 274 class pickleformatter(baseformatter):
274 def __init__(self, ui, topic, opts):
275 def __init__(self, ui, out, topic, opts):
275 276 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
277 self._out = out
276 278 self._data = []
277 279 def _showitem(self):
278 280 self._data.append(self._item)
279 281 def end(self):
280 282 baseformatter.end(self)
281 self._ui.write(pickle.dumps(self._data))
283 self._out.write(pickle.dumps(self._data))
282 284
283 285 def _jsonifyobj(v):
284 286 if isinstance(v, dict):
285 287 xs = ['"%s": %s' % (encoding.jsonescape(k), _jsonifyobj(u))
286 288 for k, u in sorted(v.iteritems())]
287 289 return '{' + ', '.join(xs) + '}'
288 290 elif isinstance(v, (list, tuple)):
289 291 return '[' + ', '.join(_jsonifyobj(e) for e in v) + ']'
290 292 elif v is None:
291 293 return 'null'
292 294 elif v is True:
293 295 return 'true'
294 296 elif v is False:
295 297 return 'false'
296 298 elif isinstance(v, (int, float)):
297 299 return str(v)
298 300 else:
299 301 return '"%s"' % encoding.jsonescape(v)
300 302
301 303 class jsonformatter(baseformatter):
302 def __init__(self, ui, topic, opts):
304 def __init__(self, ui, out, topic, opts):
303 305 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
304 self._ui.write("[")
306 self._out = out
307 self._out.write("[")
305 308 self._ui._first = True
306 309 def _showitem(self):
307 310 if self._ui._first:
308 311 self._ui._first = False
309 312 else:
310 self._ui.write(",")
313 self._out.write(",")
311 314
312 self._ui.write("\n {\n")
315 self._out.write("\n {\n")
313 316 first = True
314 317 for k, v in sorted(self._item.items()):
315 318 if first:
316 319 first = False
317 320 else:
318 self._ui.write(",\n")
319 self._ui.write(' "%s": %s' % (k, _jsonifyobj(v)))
320 self._ui.write("\n }")
321 self._out.write(",\n")
322 self._out.write(' "%s": %s' % (k, _jsonifyobj(v)))
323 self._out.write("\n }")
321 324 def end(self):
322 325 baseformatter.end(self)
323 self._ui.write("\n]\n")
326 self._out.write("\n]\n")
324 327
325 328 class _templateconverter(object):
326 329 '''convert non-primitive data types to be processed by templater'''
327 330 @staticmethod
328 331 def formatdate(date, fmt):
329 332 '''return date tuple'''
330 333 return date
331 334 @staticmethod
332 335 def formatdict(data, key, value, fmt, sep):
333 336 '''build object that can be evaluated as either plain string or dict'''
334 337 data = util.sortdict(_iteritems(data))
335 338 def f():
336 339 yield _plainconverter.formatdict(data, key, value, fmt, sep)
337 340 return templatekw._hybrid(f(), data, lambda k: {key: k, value: data[k]},
338 341 lambda d: fmt % (d[key], d[value]))
339 342 @staticmethod
340 343 def formatlist(data, name, fmt, sep):
341 344 '''build object that can be evaluated as either plain string or list'''
342 345 data = list(data)
343 346 def f():
344 347 yield _plainconverter.formatlist(data, name, fmt, sep)
345 348 return templatekw._hybrid(f(), data, lambda x: {name: x},
346 349 lambda d: fmt % d[name])
347 350
348 351 class templateformatter(baseformatter):
349 def __init__(self, ui, topic, opts):
352 def __init__(self, ui, out, topic, opts):
350 353 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
354 self._out = out
351 355 self._topic = topic
352 356 self._t = gettemplater(ui, topic, opts.get('template', ''),
353 357 cache=templatekw.defaulttempl)
354 358 self._cache = {} # for templatekw/funcs to store reusable data
355 359 def context(self, **ctxs):
356 360 '''insert context objects to be used to render template keywords'''
357 361 assert all(k == 'ctx' for k in ctxs)
358 362 self._item.update(ctxs)
359 363 def _showitem(self):
360 364 # TODO: add support for filectx. probably each template keyword or
361 365 # function will have to declare dependent resources. e.g.
362 366 # @templatekeyword(..., requires=('ctx',))
363 367 if 'ctx' in self._item:
364 368 props = templatekw.keywords.copy()
365 369 # explicitly-defined fields precede templatekw
366 370 props.update(self._item)
367 371 # but template resources must be always available
368 372 props['templ'] = self._t
369 373 props['repo'] = props['ctx'].repo()
370 374 props['revcache'] = {}
371 375 else:
372 376 props = self._item
373 377 g = self._t(self._topic, ui=self._ui, cache=self._cache, **props)
374 self._ui.write(templater.stringify(g))
378 self._out.write(templater.stringify(g))
375 379
376 380 def lookuptemplate(ui, topic, tmpl):
377 381 # looks like a literal template?
378 382 if '{' in tmpl:
379 383 return tmpl, None
380 384
381 385 # perhaps a stock style?
382 386 if not os.path.split(tmpl)[0]:
383 387 mapname = (templater.templatepath('map-cmdline.' + tmpl)
384 388 or templater.templatepath(tmpl))
385 389 if mapname and os.path.isfile(mapname):
386 390 return None, mapname
387 391
388 392 # perhaps it's a reference to [templates]
389 393 t = ui.config('templates', tmpl)
390 394 if t:
391 395 return templater.unquotestring(t), None
392 396
393 397 if tmpl == 'list':
394 398 ui.write(_("available styles: %s\n") % templater.stylelist())
395 399 raise error.Abort(_("specify a template"))
396 400
397 401 # perhaps it's a path to a map or a template
398 402 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
399 403 # is it a mapfile for a style?
400 404 if os.path.basename(tmpl).startswith("map-"):
401 405 return None, os.path.realpath(tmpl)
402 406 tmpl = open(tmpl).read()
403 407 return tmpl, None
404 408
405 409 # constant string?
406 410 return tmpl, None
407 411
408 412 def gettemplater(ui, topic, spec, cache=None):
409 413 tmpl, mapfile = lookuptemplate(ui, topic, spec)
410 414 assert not (tmpl and mapfile)
411 415 if mapfile:
412 416 return templater.templater.frommapfile(mapfile, cache=cache)
413 417 return maketemplater(ui, topic, tmpl, cache=cache)
414 418
415 419 def maketemplater(ui, topic, tmpl, cache=None):
416 420 """Create a templater from a string template 'tmpl'"""
417 421 aliases = ui.configitems('templatealias')
418 422 t = templater.templater(cache=cache, aliases=aliases)
419 423 if tmpl:
420 424 t.cache[topic] = tmpl
421 425 return t
422 426
423 427 def formatter(ui, topic, opts):
424 428 template = opts.get("template", "")
425 429 if template == "json":
426 return jsonformatter(ui, topic, opts)
430 return jsonformatter(ui, ui, topic, opts)
427 431 elif template == "pickle":
428 return pickleformatter(ui, topic, opts)
432 return pickleformatter(ui, ui, topic, opts)
429 433 elif template == "debug":
430 return debugformatter(ui, topic, opts)
434 return debugformatter(ui, ui, topic, opts)
431 435 elif template != "":
432 return templateformatter(ui, topic, opts)
436 return templateformatter(ui, ui, topic, opts)
433 437 # developer config: ui.formatdebug
434 438 elif ui.configbool('ui', 'formatdebug'):
435 return debugformatter(ui, topic, opts)
439 return debugformatter(ui, ui, topic, opts)
436 440 # deprecated config: ui.formatjson
437 441 elif ui.configbool('ui', 'formatjson'):
438 return jsonformatter(ui, topic, opts)
442 return jsonformatter(ui, ui, topic, opts)
439 443 return plainformatter(ui, topic, opts)
General Comments 0
You need to be logged in to leave comments. Login now