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