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