##// END OF EJS Templates
formatter: remove unused private attributes from baseformatter...
Yuya Nishihara -
r37614:8c121a98 default
parent child Browse files
Show More
@@ -1,610 +1,608 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', tmpl=b'{reponame}'))
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 templateutil,
128 128 util,
129 129 )
130 130 from .utils import dateutil
131 131
132 132 pickle = util.pickle
133 133
134 134 class _nullconverter(object):
135 135 '''convert non-primitive data types to be processed by formatter'''
136 136
137 137 # set to True if context object should be stored as item
138 138 storecontext = False
139 139
140 140 @staticmethod
141 141 def wrapnested(data, tmpl, sep):
142 142 '''wrap nested data by appropriate type'''
143 143 return data
144 144 @staticmethod
145 145 def formatdate(date, fmt):
146 146 '''convert date tuple to appropriate format'''
147 147 return date
148 148 @staticmethod
149 149 def formatdict(data, key, value, fmt, sep):
150 150 '''convert dict or key-value pairs to appropriate dict format'''
151 151 # use plain dict instead of util.sortdict so that data can be
152 152 # serialized as a builtin dict in pickle output
153 153 return dict(data)
154 154 @staticmethod
155 155 def formatlist(data, name, fmt, sep):
156 156 '''convert iterable to appropriate list format'''
157 157 return list(data)
158 158
159 159 class baseformatter(object):
160 160 def __init__(self, ui, topic, opts, converter):
161 161 self._ui = ui
162 162 self._topic = topic
163 self._style = opts.get("style")
164 self._template = opts.get("template")
165 163 self._converter = converter
166 164 self._item = None
167 165 # function to convert node to string suitable for this output
168 166 self.hexfunc = hex
169 167 def __enter__(self):
170 168 return self
171 169 def __exit__(self, exctype, excvalue, traceback):
172 170 if exctype is None:
173 171 self.end()
174 172 def _showitem(self):
175 173 '''show a formatted item once all data is collected'''
176 174 def startitem(self):
177 175 '''begin an item in the format list'''
178 176 if self._item is not None:
179 177 self._showitem()
180 178 self._item = {}
181 179 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
182 180 '''convert date tuple to appropriate format'''
183 181 return self._converter.formatdate(date, fmt)
184 182 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
185 183 '''convert dict or key-value pairs to appropriate dict format'''
186 184 return self._converter.formatdict(data, key, value, fmt, sep)
187 185 def formatlist(self, data, name, fmt=None, sep=' '):
188 186 '''convert iterable to appropriate list format'''
189 187 # name is mandatory argument for now, but it could be optional if
190 188 # we have default template keyword, e.g. {item}
191 189 return self._converter.formatlist(data, name, fmt, sep)
192 190 def context(self, **ctxs):
193 191 '''insert context objects to be used to render template keywords'''
194 192 ctxs = pycompat.byteskwargs(ctxs)
195 193 assert all(k in {'ctx', 'fctx'} for k in ctxs)
196 194 if self._converter.storecontext:
197 195 self._item.update(ctxs)
198 196 def data(self, **data):
199 197 '''insert data into item that's not shown in default output'''
200 198 data = pycompat.byteskwargs(data)
201 199 self._item.update(data)
202 200 def write(self, fields, deftext, *fielddata, **opts):
203 201 '''do default text output while assigning data to item'''
204 202 fieldkeys = fields.split()
205 203 assert len(fieldkeys) == len(fielddata)
206 204 self._item.update(zip(fieldkeys, fielddata))
207 205 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
208 206 '''do conditional write (primarily for plain formatter)'''
209 207 fieldkeys = fields.split()
210 208 assert len(fieldkeys) == len(fielddata)
211 209 self._item.update(zip(fieldkeys, fielddata))
212 210 def plain(self, text, **opts):
213 211 '''show raw text for non-templated mode'''
214 212 def isplain(self):
215 213 '''check for plain formatter usage'''
216 214 return False
217 215 def nested(self, field, tmpl=None, sep=''):
218 216 '''sub formatter to store nested data in the specified field'''
219 217 data = []
220 218 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
221 219 return _nestedformatter(self._ui, self._converter, data)
222 220 def end(self):
223 221 '''end output for the formatter'''
224 222 if self._item is not None:
225 223 self._showitem()
226 224
227 225 def nullformatter(ui, topic):
228 226 '''formatter that prints nothing'''
229 227 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
230 228
231 229 class _nestedformatter(baseformatter):
232 230 '''build sub items and store them in the parent formatter'''
233 231 def __init__(self, ui, converter, data):
234 232 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
235 233 self._data = data
236 234 def _showitem(self):
237 235 self._data.append(self._item)
238 236
239 237 def _iteritems(data):
240 238 '''iterate key-value pairs in stable order'''
241 239 if isinstance(data, dict):
242 240 return sorted(data.iteritems())
243 241 return data
244 242
245 243 class _plainconverter(object):
246 244 '''convert non-primitive data types to text'''
247 245
248 246 storecontext = False
249 247
250 248 @staticmethod
251 249 def wrapnested(data, tmpl, sep):
252 250 raise error.ProgrammingError('plainformatter should never be nested')
253 251 @staticmethod
254 252 def formatdate(date, fmt):
255 253 '''stringify date tuple in the given format'''
256 254 return dateutil.datestr(date, fmt)
257 255 @staticmethod
258 256 def formatdict(data, key, value, fmt, sep):
259 257 '''stringify key-value pairs separated by sep'''
260 258 prefmt = pycompat.identity
261 259 if fmt is None:
262 260 fmt = '%s=%s'
263 261 prefmt = pycompat.bytestr
264 262 return sep.join(fmt % (prefmt(k), prefmt(v))
265 263 for k, v in _iteritems(data))
266 264 @staticmethod
267 265 def formatlist(data, name, fmt, sep):
268 266 '''stringify iterable separated by sep'''
269 267 prefmt = pycompat.identity
270 268 if fmt is None:
271 269 fmt = '%s'
272 270 prefmt = pycompat.bytestr
273 271 return sep.join(fmt % prefmt(e) for e in data)
274 272
275 273 class plainformatter(baseformatter):
276 274 '''the default text output scheme'''
277 275 def __init__(self, ui, out, topic, opts):
278 276 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
279 277 if ui.debugflag:
280 278 self.hexfunc = hex
281 279 else:
282 280 self.hexfunc = short
283 281 if ui is out:
284 282 self._write = ui.write
285 283 else:
286 284 self._write = lambda s, **opts: out.write(s)
287 285 def startitem(self):
288 286 pass
289 287 def data(self, **data):
290 288 pass
291 289 def write(self, fields, deftext, *fielddata, **opts):
292 290 self._write(deftext % fielddata, **opts)
293 291 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
294 292 '''do conditional write'''
295 293 if cond:
296 294 self._write(deftext % fielddata, **opts)
297 295 def plain(self, text, **opts):
298 296 self._write(text, **opts)
299 297 def isplain(self):
300 298 return True
301 299 def nested(self, field, tmpl=None, sep=''):
302 300 # nested data will be directly written to ui
303 301 return self
304 302 def end(self):
305 303 pass
306 304
307 305 class debugformatter(baseformatter):
308 306 def __init__(self, ui, out, topic, opts):
309 307 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
310 308 self._out = out
311 309 self._out.write("%s = [\n" % self._topic)
312 310 def _showitem(self):
313 311 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
314 312 def end(self):
315 313 baseformatter.end(self)
316 314 self._out.write("]\n")
317 315
318 316 class pickleformatter(baseformatter):
319 317 def __init__(self, ui, out, topic, opts):
320 318 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
321 319 self._out = out
322 320 self._data = []
323 321 def _showitem(self):
324 322 self._data.append(self._item)
325 323 def end(self):
326 324 baseformatter.end(self)
327 325 self._out.write(pickle.dumps(self._data))
328 326
329 327 class jsonformatter(baseformatter):
330 328 def __init__(self, ui, out, topic, opts):
331 329 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
332 330 self._out = out
333 331 self._out.write("[")
334 332 self._first = True
335 333 def _showitem(self):
336 334 if self._first:
337 335 self._first = False
338 336 else:
339 337 self._out.write(",")
340 338
341 339 self._out.write("\n {\n")
342 340 first = True
343 341 for k, v in sorted(self._item.items()):
344 342 if first:
345 343 first = False
346 344 else:
347 345 self._out.write(",\n")
348 346 u = templatefilters.json(v, paranoid=False)
349 347 self._out.write(' "%s": %s' % (k, u))
350 348 self._out.write("\n }")
351 349 def end(self):
352 350 baseformatter.end(self)
353 351 self._out.write("\n]\n")
354 352
355 353 class _templateconverter(object):
356 354 '''convert non-primitive data types to be processed by templater'''
357 355
358 356 storecontext = True
359 357
360 358 @staticmethod
361 359 def wrapnested(data, tmpl, sep):
362 360 '''wrap nested data by templatable type'''
363 361 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
364 362 @staticmethod
365 363 def formatdate(date, fmt):
366 364 '''return date tuple'''
367 365 return date
368 366 @staticmethod
369 367 def formatdict(data, key, value, fmt, sep):
370 368 '''build object that can be evaluated as either plain string or dict'''
371 369 data = util.sortdict(_iteritems(data))
372 370 def f():
373 371 yield _plainconverter.formatdict(data, key, value, fmt, sep)
374 372 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
375 373 gen=f)
376 374 @staticmethod
377 375 def formatlist(data, name, fmt, sep):
378 376 '''build object that can be evaluated as either plain string or list'''
379 377 data = list(data)
380 378 def f():
381 379 yield _plainconverter.formatlist(data, name, fmt, sep)
382 380 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
383 381
384 382 class templateformatter(baseformatter):
385 383 def __init__(self, ui, out, topic, opts):
386 384 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
387 385 self._out = out
388 386 spec = lookuptemplate(ui, topic, opts.get('template', ''))
389 387 self._tref = spec.ref
390 388 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
391 389 resources=templateresources(ui),
392 390 cache=templatekw.defaulttempl)
393 391 self._parts = templatepartsmap(spec, self._t,
394 392 ['docheader', 'docfooter', 'separator'])
395 393 self._counter = itertools.count()
396 394 self._renderitem('docheader', {})
397 395
398 396 def _showitem(self):
399 397 item = self._item.copy()
400 398 item['index'] = index = next(self._counter)
401 399 if index > 0:
402 400 self._renderitem('separator', {})
403 401 self._renderitem(self._tref, item)
404 402
405 403 def _renderitem(self, part, item):
406 404 if part not in self._parts:
407 405 return
408 406 ref = self._parts[part]
409 407 self._out.write(self._t.render(ref, item))
410 408
411 409 def end(self):
412 410 baseformatter.end(self)
413 411 self._renderitem('docfooter', {})
414 412
415 413 templatespec = collections.namedtuple(r'templatespec',
416 414 r'ref tmpl mapfile')
417 415
418 416 def lookuptemplate(ui, topic, tmpl):
419 417 """Find the template matching the given -T/--template spec 'tmpl'
420 418
421 419 'tmpl' can be any of the following:
422 420
423 421 - a literal template (e.g. '{rev}')
424 422 - a map-file name or path (e.g. 'changelog')
425 423 - a reference to [templates] in config file
426 424 - a path to raw template file
427 425
428 426 A map file defines a stand-alone template environment. If a map file
429 427 selected, all templates defined in the file will be loaded, and the
430 428 template matching the given topic will be rendered. Aliases won't be
431 429 loaded from user config, but from the map file.
432 430
433 431 If no map file selected, all templates in [templates] section will be
434 432 available as well as aliases in [templatealias].
435 433 """
436 434
437 435 # looks like a literal template?
438 436 if '{' in tmpl:
439 437 return templatespec('', tmpl, None)
440 438
441 439 # perhaps a stock style?
442 440 if not os.path.split(tmpl)[0]:
443 441 mapname = (templater.templatepath('map-cmdline.' + tmpl)
444 442 or templater.templatepath(tmpl))
445 443 if mapname and os.path.isfile(mapname):
446 444 return templatespec(topic, None, mapname)
447 445
448 446 # perhaps it's a reference to [templates]
449 447 if ui.config('templates', tmpl):
450 448 return templatespec(tmpl, None, None)
451 449
452 450 if tmpl == 'list':
453 451 ui.write(_("available styles: %s\n") % templater.stylelist())
454 452 raise error.Abort(_("specify a template"))
455 453
456 454 # perhaps it's a path to a map or a template
457 455 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
458 456 # is it a mapfile for a style?
459 457 if os.path.basename(tmpl).startswith("map-"):
460 458 return templatespec(topic, None, os.path.realpath(tmpl))
461 459 with util.posixfile(tmpl, 'rb') as f:
462 460 tmpl = f.read()
463 461 return templatespec('', tmpl, None)
464 462
465 463 # constant string?
466 464 return templatespec('', tmpl, None)
467 465
468 466 def templatepartsmap(spec, t, partnames):
469 467 """Create a mapping of {part: ref}"""
470 468 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
471 469 if spec.mapfile:
472 470 partsmap.update((p, p) for p in partnames if p in t)
473 471 elif spec.ref:
474 472 for part in partnames:
475 473 ref = '%s:%s' % (spec.ref, part) # select config sub-section
476 474 if ref in t:
477 475 partsmap[part] = ref
478 476 return partsmap
479 477
480 478 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
481 479 """Create a templater from either a literal template or loading from
482 480 a map file"""
483 481 assert not (spec.tmpl and spec.mapfile)
484 482 if spec.mapfile:
485 483 frommapfile = templater.templater.frommapfile
486 484 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
487 485 cache=cache)
488 486 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
489 487 cache=cache)
490 488
491 489 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
492 490 """Create a templater from a string template 'tmpl'"""
493 491 aliases = ui.configitems('templatealias')
494 492 t = templater.templater(defaults=defaults, resources=resources,
495 493 cache=cache, aliases=aliases)
496 494 t.cache.update((k, templater.unquotestring(v))
497 495 for k, v in ui.configitems('templates'))
498 496 if tmpl:
499 497 t.cache[''] = tmpl
500 498 return t
501 499
502 500 class templateresources(templater.resourcemapper):
503 501 """Resource mapper designed for the default templatekw and function"""
504 502
505 503 def __init__(self, ui, repo=None):
506 504 self._resmap = {
507 505 'cache': {}, # for templatekw/funcs to store reusable data
508 506 'repo': repo,
509 507 'ui': ui,
510 508 }
511 509
512 510 def availablekeys(self, context, mapping):
513 511 return {k for k, g in self._gettermap.iteritems()
514 512 if g(self, context, mapping, k) is not None}
515 513
516 514 def knownkeys(self):
517 515 return self._knownkeys
518 516
519 517 def lookup(self, context, mapping, key):
520 518 get = self._gettermap.get(key)
521 519 if not get:
522 520 return None
523 521 return get(self, context, mapping, key)
524 522
525 523 def populatemap(self, context, origmapping, newmapping):
526 524 mapping = {}
527 525 if self._hasctx(newmapping):
528 526 mapping['revcache'] = {} # per-ctx cache
529 527 if (('node' in origmapping or self._hasctx(origmapping))
530 528 and ('node' in newmapping or self._hasctx(newmapping))):
531 529 orignode = templateutil.runsymbol(context, origmapping, 'node')
532 530 mapping['originalnode'] = orignode
533 531 return mapping
534 532
535 533 def _getsome(self, context, mapping, key):
536 534 v = mapping.get(key)
537 535 if v is not None:
538 536 return v
539 537 return self._resmap.get(key)
540 538
541 539 def _hasctx(self, mapping):
542 540 return 'ctx' in mapping or 'fctx' in mapping
543 541
544 542 def _getctx(self, context, mapping, key):
545 543 ctx = mapping.get('ctx')
546 544 if ctx is not None:
547 545 return ctx
548 546 fctx = mapping.get('fctx')
549 547 if fctx is not None:
550 548 return fctx.changectx()
551 549
552 550 def _getrepo(self, context, mapping, key):
553 551 ctx = self._getctx(context, mapping, 'ctx')
554 552 if ctx is not None:
555 553 return ctx.repo()
556 554 return self._getsome(context, mapping, key)
557 555
558 556 _gettermap = {
559 557 'cache': _getsome,
560 558 'ctx': _getctx,
561 559 'fctx': _getsome,
562 560 'repo': _getrepo,
563 561 'revcache': _getsome,
564 562 'ui': _getsome,
565 563 }
566 564 _knownkeys = set(_gettermap.keys())
567 565
568 566 def formatter(ui, out, topic, opts):
569 567 template = opts.get("template", "")
570 568 if template == "json":
571 569 return jsonformatter(ui, out, topic, opts)
572 570 elif template == "pickle":
573 571 return pickleformatter(ui, out, topic, opts)
574 572 elif template == "debug":
575 573 return debugformatter(ui, out, topic, opts)
576 574 elif template != "":
577 575 return templateformatter(ui, out, topic, opts)
578 576 # developer config: ui.formatdebug
579 577 elif ui.configbool('ui', 'formatdebug'):
580 578 return debugformatter(ui, out, topic, opts)
581 579 # deprecated config: ui.formatjson
582 580 elif ui.configbool('ui', 'formatjson'):
583 581 return jsonformatter(ui, out, topic, opts)
584 582 return plainformatter(ui, out, topic, opts)
585 583
586 584 @contextlib.contextmanager
587 585 def openformatter(ui, filename, topic, opts):
588 586 """Create a formatter that writes outputs to the specified file
589 587
590 588 Must be invoked using the 'with' statement.
591 589 """
592 590 with util.posixfile(filename, 'wb') as out:
593 591 with formatter(ui, out, topic, opts) as fm:
594 592 yield fm
595 593
596 594 @contextlib.contextmanager
597 595 def _neverending(fm):
598 596 yield fm
599 597
600 598 def maybereopen(fm, filename, opts):
601 599 """Create a formatter backed by file if filename specified, else return
602 600 the given formatter
603 601
604 602 Must be invoked using the 'with' statement. This will never call fm.end()
605 603 of the given formatter.
606 604 """
607 605 if filename:
608 606 return openformatter(fm._ui, filename, fm._topic, opts)
609 607 else:
610 608 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now