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