##// END OF EJS Templates
formatter: support json formatting of long type...
Gregory Szorc -
r31396:ab3e9eab default
parent child Browse files
Show More
@@ -1,443 +1,443 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 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 264 def __init__(self, ui, out, topic, opts):
265 265 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
266 266 self._out = out
267 267 self._out.write("%s = [\n" % self._topic)
268 268 def _showitem(self):
269 269 self._out.write(" " + repr(self._item) + ",\n")
270 270 def end(self):
271 271 baseformatter.end(self)
272 272 self._out.write("]\n")
273 273
274 274 class pickleformatter(baseformatter):
275 275 def __init__(self, ui, out, topic, opts):
276 276 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
277 277 self._out = out
278 278 self._data = []
279 279 def _showitem(self):
280 280 self._data.append(self._item)
281 281 def end(self):
282 282 baseformatter.end(self)
283 283 self._out.write(pickle.dumps(self._data))
284 284
285 285 def _jsonifyobj(v):
286 286 if isinstance(v, dict):
287 287 xs = ['"%s": %s' % (encoding.jsonescape(k), _jsonifyobj(u))
288 288 for k, u in sorted(v.iteritems())]
289 289 return '{' + ', '.join(xs) + '}'
290 290 elif isinstance(v, (list, tuple)):
291 291 return '[' + ', '.join(_jsonifyobj(e) for e in v) + ']'
292 292 elif v is None:
293 293 return 'null'
294 294 elif v is True:
295 295 return 'true'
296 296 elif v is False:
297 297 return 'false'
298 elif isinstance(v, (int, float)):
298 elif isinstance(v, (int, long, float)):
299 299 return str(v)
300 300 else:
301 301 return '"%s"' % encoding.jsonescape(v)
302 302
303 303 class jsonformatter(baseformatter):
304 304 def __init__(self, ui, out, topic, opts):
305 305 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
306 306 self._out = out
307 307 self._out.write("[")
308 308 self._first = True
309 309 def _showitem(self):
310 310 if self._first:
311 311 self._first = False
312 312 else:
313 313 self._out.write(",")
314 314
315 315 self._out.write("\n {\n")
316 316 first = True
317 317 for k, v in sorted(self._item.items()):
318 318 if first:
319 319 first = False
320 320 else:
321 321 self._out.write(",\n")
322 322 self._out.write(' "%s": %s' % (k, _jsonifyobj(v)))
323 323 self._out.write("\n }")
324 324 def end(self):
325 325 baseformatter.end(self)
326 326 self._out.write("\n]\n")
327 327
328 328 class _templateconverter(object):
329 329 '''convert non-primitive data types to be processed by templater'''
330 330 @staticmethod
331 331 def formatdate(date, fmt):
332 332 '''return date tuple'''
333 333 return date
334 334 @staticmethod
335 335 def formatdict(data, key, value, fmt, sep):
336 336 '''build object that can be evaluated as either plain string or dict'''
337 337 data = util.sortdict(_iteritems(data))
338 338 def f():
339 339 yield _plainconverter.formatdict(data, key, value, fmt, sep)
340 340 return templatekw._hybrid(f(), data, lambda k: {key: k, value: data[k]},
341 341 lambda d: fmt % (d[key], d[value]))
342 342 @staticmethod
343 343 def formatlist(data, name, fmt, sep):
344 344 '''build object that can be evaluated as either plain string or list'''
345 345 data = list(data)
346 346 def f():
347 347 yield _plainconverter.formatlist(data, name, fmt, sep)
348 348 return templatekw._hybrid(f(), data, lambda x: {name: x},
349 349 lambda d: fmt % d[name])
350 350
351 351 class templateformatter(baseformatter):
352 352 def __init__(self, ui, out, topic, opts):
353 353 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
354 354 self._out = out
355 355 self._topic = topic
356 356 self._t = gettemplater(ui, topic, opts.get('template', ''),
357 357 cache=templatekw.defaulttempl)
358 358 self._cache = {} # for templatekw/funcs to store reusable data
359 359 def context(self, **ctxs):
360 360 '''insert context objects to be used to render template keywords'''
361 361 assert all(k == 'ctx' for k in ctxs)
362 362 self._item.update(ctxs)
363 363 def _showitem(self):
364 364 # TODO: add support for filectx. probably each template keyword or
365 365 # function will have to declare dependent resources. e.g.
366 366 # @templatekeyword(..., requires=('ctx',))
367 367 if 'ctx' in self._item:
368 368 props = templatekw.keywords.copy()
369 369 # explicitly-defined fields precede templatekw
370 370 props.update(self._item)
371 371 # but template resources must be always available
372 372 props['templ'] = self._t
373 373 props['repo'] = props['ctx'].repo()
374 374 props['revcache'] = {}
375 375 else:
376 376 props = self._item
377 377 g = self._t(self._topic, ui=self._ui, cache=self._cache, **props)
378 378 self._out.write(templater.stringify(g))
379 379
380 380 def lookuptemplate(ui, topic, tmpl):
381 381 # looks like a literal template?
382 382 if '{' in tmpl:
383 383 return tmpl, None
384 384
385 385 # perhaps a stock style?
386 386 if not os.path.split(tmpl)[0]:
387 387 mapname = (templater.templatepath('map-cmdline.' + tmpl)
388 388 or templater.templatepath(tmpl))
389 389 if mapname and os.path.isfile(mapname):
390 390 return None, mapname
391 391
392 392 # perhaps it's a reference to [templates]
393 393 t = ui.config('templates', tmpl)
394 394 if t:
395 395 return templater.unquotestring(t), None
396 396
397 397 if tmpl == 'list':
398 398 ui.write(_("available styles: %s\n") % templater.stylelist())
399 399 raise error.Abort(_("specify a template"))
400 400
401 401 # perhaps it's a path to a map or a template
402 402 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
403 403 # is it a mapfile for a style?
404 404 if os.path.basename(tmpl).startswith("map-"):
405 405 return None, os.path.realpath(tmpl)
406 406 tmpl = open(tmpl).read()
407 407 return tmpl, None
408 408
409 409 # constant string?
410 410 return tmpl, None
411 411
412 412 def gettemplater(ui, topic, spec, cache=None):
413 413 tmpl, mapfile = lookuptemplate(ui, topic, spec)
414 414 assert not (tmpl and mapfile)
415 415 if mapfile:
416 416 return templater.templater.frommapfile(mapfile, cache=cache)
417 417 return maketemplater(ui, topic, tmpl, cache=cache)
418 418
419 419 def maketemplater(ui, topic, tmpl, cache=None):
420 420 """Create a templater from a string template 'tmpl'"""
421 421 aliases = ui.configitems('templatealias')
422 422 t = templater.templater(cache=cache, aliases=aliases)
423 423 if tmpl:
424 424 t.cache[topic] = tmpl
425 425 return t
426 426
427 427 def formatter(ui, topic, opts):
428 428 template = opts.get("template", "")
429 429 if template == "json":
430 430 return jsonformatter(ui, ui, topic, opts)
431 431 elif template == "pickle":
432 432 return pickleformatter(ui, ui, topic, opts)
433 433 elif template == "debug":
434 434 return debugformatter(ui, ui, topic, opts)
435 435 elif template != "":
436 436 return templateformatter(ui, ui, topic, opts)
437 437 # developer config: ui.formatdebug
438 438 elif ui.configbool('ui', 'formatdebug'):
439 439 return debugformatter(ui, ui, topic, opts)
440 440 # deprecated config: ui.formatjson
441 441 elif ui.configbool('ui', 'formatjson'):
442 442 return jsonformatter(ui, ui, topic, opts)
443 443 return plainformatter(ui, topic, opts)
General Comments 0
You need to be logged in to leave comments. Login now